Files
MSPPPPaM/auth/auth.php
T

353 lines
12 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 s tlačítkem "Odhlásit se"
// a (pro admina) odkazem na admin rozhraní.
// Neprázdný pouze pokud je uživatel přihlášen.
// Použití: echo $auth_logout_html;
//
// $auth_login_html ... HTML formulář pro přihlášení (email, heslo,
// zapamatovat si mě, CSRF token).
// Neprázdný pouze pokud uživatel NENÍ přihlášen.
// Data odesílá na AUTH_LOGIN_URL (login.php),
// které po přihlášení přesměruje zpět na
// aktuální stránku.
// Použití: echo $auth_login_html;
//
// Typické použití stránka vyžadující přihlášení
// (VYZADOVAT_PRIHLASENI = true v config.php):
//
// require_once 'auth/auth.php';
// // Sem se dostane jen přihlášený uživatel.
// // Přihlášený email: $auth_uzivatel['email']
// // Odhlašovací tlačítko: echo $auth_logout_html;
//
// Typické použití stránka s omezeným přístupem
// (VYZADOVAT_PRIHLASENI = false v config.php):
//
// require_once 'auth/auth.php';
// // if ($auth_prihlasen): zobraz přihlášený obsah + echo $auth_logout_html;
// // else: zobraz veřejný obsah + echo $auth_login_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 = '';
$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 zajistíme, že vždy existuje
// ------------------------------------------------------------
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// ------------------------------------------------------------
// PŘIPRAVENÉ HTML PROMĚNNÉ
// ------------------------------------------------------------
// -- Odhlašovací formulář ------------------------------------
// Neprázdný pouze pokud je uživatel přihlášen.
// Pro admina obsahuje navíc odkaz na administrační rozhraní.
// Použití na stránce: echo $auth_logout_html;
if ($auth_prihlasen) {
$auth_logout_html = '';
// Odkaz na admin rozhraní pouze pro admina
if ($auth_uzivatel['admin']) {
$auth_logout_html .=
'<a href="' . htmlspecialchars(AUTH_ADMIN_URL) . '">Administrace</a> | ';
}
// Přihlášený uživatel a tlačítko odhlášení
$auth_logout_html .=
htmlspecialchars($auth_uzivatel['email'])
. ' | '
. '<form method="POST" action="' . htmlspecialchars(AUTH_LOGOUT_URL) . '"'
. ' style="display:inline;">'
. '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars($_SESSION['csrf_token']) . '">'
. '<button type="submit">Odhlásit se</button>'
. '</form>';
}
// -- Přihlašovací formulář -----------------------------------
// Neprázdný pouze pokud uživatel NENÍ přihlášen.
// Odesílá data na login.php, které zpracuje přihlášení
// a přesměruje uživatele zpět na původní stránku.
// Použití na stránce: echo $auth_login_html;
if (!$auth_prihlasen) {
// 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'] ?? '';
$auth_login_html =
'<form method="POST" action="' . htmlspecialchars(AUTH_LOGIN_URL) . '">'
// CSRF token ochrana před podvrženými požadavky
. '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars($_SESSION['csrf_token']) . '">'
// Aktuální URL login.php nás po přihlášení přesměruje zpět
. '<input type="hidden" name="redirect_url" value="'
. htmlspecialchars($aktualni_url) . '">'
. '<p>'
. '<label for="auth-email">Email:</label><br>'
. '<input type="email" id="auth-email" name="email"'
. ' required autocomplete="email">'
. '</p>'
. '<p>'
. '<label for="auth-heslo">Heslo:</label><br>'
. '<input type="password" id="auth-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>';
}