Files
MSPPPPaM/auth/auth.php
T
stepan 220124a54a [FÁZE-1][config] Přidána konstanta AUTH_LOGOUT_URL
[FÁZE-1][auth] Přidána proměnná $auth_logout_html s připraveným odhlašovacím formulářem
2026-03-17 00:02:43 +01:00

261 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// ============================================================
// HLAVNÍ SOUBOR AUTENTIZACE
// ============================================================
// Tento soubor vložíš na začátek každé stránky, kterou chceš
// chránit přihlášením:
//
// require_once 'auth/auth.php';
//
// Po jeho načtení máš k dispozici tyto proměnné:
//
// $auth_prihlasen ... true / false (vždy nastaveno)
// $auth_uzivatel['id'] ... ID přihlášeného uživatele (nebo null)
// $auth_uzivatel['email'] ... email přihlášeného uživatele (nebo null)
// $auth_uzivatel['admin'] ... true / false (vždy nastaveno)
// $auth_logout_html ... HTML formulář s tlačítkem odhlášení,
// nebo prázdný řetězec pokud není přihlášen
//
// Použití odhlašovacího tlačítka na chráněné stránce:
//
// echo $auth_logout_html;
//
// ============================================================
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, // 0 = cookie platí do zavření prohlížeče
// (trvalost zajišťuje remember me, ne session)
'path' => '/',
'httponly' => true, // JavaScript k cookie nemá přístup
'samesite' => 'Strict', // ochrana před CSRF
// 'secure' => true, // odkomentuj pokud web běží na HTTPS
]);
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);
exit;
}
// ------------------------------------------------------------
// PŘIPRAVENÝ HTML KÓD PRO ODHLAŠOVACÍ TLAČÍTKO
// ------------------------------------------------------------
// 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 =
'<form method="POST" action="' . htmlspecialchars(AUTH_LOGOUT_URL) . '">'
. '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars($_SESSION['csrf_token'] ?? '') . '">'
. '<button type="submit">Odhlásit se</button>'
. '</form>';
}