Files
Fociz/includes/auth.php
T

328 lines
10 KiB
PHP

<?php
/**
* Authenticatie & Autorisatie Guard voor Fociz
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/totp.php';
/**
* Bepaalt of de huidige verbinding HTTPS gebruikt (inclusief reverse proxies / OpenResty / Docker)
*/
function is_https_request(): bool {
if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') {
return true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
return true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && strtolower($_SERVER['HTTP_X_FORWARDED_SSL']) === 'on') {
return true;
}
if (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443) {
return true;
}
if (defined('APP_URL') && str_starts_with(APP_URL, 'https://')) {
return true;
}
return false;
}
function start_secure_session(): void {
if (session_status() === PHP_SESSION_NONE) {
// Zorg voor een ruime sessieduur (30 dagen)
ini_set('session.gc_maxlifetime', '2592000');
ini_set('session.cookie_lifetime', '2592000');
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
$isSecure = is_https_request();
session_set_cookie_params([
'lifetime' => 30 * 24 * 60 * 60, // 30 dagen
'path' => '/',
'domain' => '',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
}
}
/**
* Controleert of er al een beheerder bestaat in MariaDB
*/
function has_admin_user(): bool {
try {
$db = get_db_connection();
$stmt = $db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'");
return ((int)$stmt->fetchColumn()) > 0;
} catch (Throwable $e) {
return false;
}
}
/**
* Garandeert dat de kolommen voor persistent sessiebeheer aanwezig zijn in MariaDB
*/
function ensure_remember_token_columns(PDO $db): void {
static $columnsChecked = false;
if ($columnsChecked) return;
$columnsChecked = true;
try {
$result = $db->query("SHOW COLUMNS FROM `users` LIKE 'remember_token'");
if (!$result || $result->rowCount() === 0) {
$db->exec("ALTER TABLE `users` ADD COLUMN `remember_token` VARCHAR(64) NULL AFTER `backup_codes`, ADD COLUMN `remember_token_expires_at` DATETIME NULL AFTER `remember_token`");
}
} catch (Throwable $e) {
// Stil negeren indien al aanwezig of tijdens migraties
}
}
/**
* Genereert en bewaart een persistente 'Onthoud mij' token in MariaDB en plaatst een veilige cookie
*/
function issue_remember_token(int $userId): string {
$rawToken = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $rawToken);
$expiresAt = date('Y-m-d H:i:s', time() + (30 * 86400)); // 30 dagen geldig
$db = get_db_connection();
ensure_remember_token_columns($db);
$stmt = $db->prepare("UPDATE users SET remember_token = ?, remember_token_expires_at = ? WHERE id = ?");
$stmt->execute([$tokenHash, $expiresAt, $userId]);
$isSecure = is_https_request();
setcookie('fociz_remember', $rawToken, [
'expires' => time() + (30 * 86400),
'path' => '/',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax'
]);
return $rawToken;
}
/**
* Verwijdert de persistente remember cookie
*/
function clear_remember_cookie(): void {
$isSecure = is_https_request();
setcookie('fociz_remember', '', [
'expires' => time() - 86400,
'path' => '/',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax'
]);
}
/**
* Haalt de huidige ingelogde gebruiker op uit de database.
* Herstelt automatisch de sessie als een geldige 'fociz_remember' cookie aanwezig is.
*/
function get_logged_in_user(): ?array {
start_secure_session();
$db = get_db_connection();
ensure_remember_token_columns($db);
// 1. Controleer actieve PHP sessie
if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) {
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, created_at FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if ($user) {
return $user;
}
// Gebruiker bestaat niet meer in database
$_SESSION = [];
}
// 2. Controleer persistente 'remember me' cookie (voorkomt ongewenst uitloggen bij refresh of herstart)
if (!empty($_COOKIE['fociz_remember'])) {
$rawToken = trim($_COOKIE['fociz_remember']);
$tokenHash = hash('sha256', $rawToken);
$stmt = $db->prepare("
SELECT id, name, email, role, two_factor_enabled, created_at
FROM users
WHERE remember_token = ? AND remember_token_expires_at > NOW()
");
$stmt->execute([$tokenHash]);
$user = $stmt->fetch();
if ($user) {
// Herstel volledige PHP sessie
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_name'] = $user['name'];
$_SESSION['user_email'] = $user['email'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['is_2fa_verified'] = true;
// Verleng direct de geldigheid van de remember token voor nog eens 30 dagen
issue_remember_token((int)$user['id']);
return $user;
} else {
clear_remember_cookie();
}
}
return null;
}
/**
* Beschermt pagina's: dwingt login + 2FA af
*/
function require_auth(bool $json = false): array {
start_secure_session();
$isAjax = $json ||
(isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json')) ||
(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
// 1. Controleer of eerste setup nodig is
if (!has_admin_user()) {
if ($isAjax) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Setup vereist']);
exit;
}
header('Location: /auth/setup.php');
exit;
}
// 2. Controleer of gebruiker ingelogd én 2FA geverifieerd is
$user = get_logged_in_user();
if (!$user) {
if ($isAjax) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Niet geauthenticeerd of sessie verlopen']);
exit;
}
header('Location: /auth/login.php');
exit;
}
return $user;
}
/**
* Dwingt beheerder (ADMIN) rol af
*/
function require_admin(): array {
$user = require_auth();
if ($user['role'] !== 'ADMIN') {
header('Location: /planner.php');
exit;
}
return $user;
}
/**
* Maakt een nieuwe uitnodiging aan voor een collega
*/
function create_invite(int $createdById, string $email, string $role = 'MEMBER'): string {
$db = get_db_connection();
$token = bin2hex(random_bytes(24));
$expiresAt = date('Y-m-d H:i:s', strtotime('+7 days'));
$stmt = $db->prepare("INSERT INTO invites (token, email, role, expires_at, created_by_id) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$token, strtolower(trim($email)), $role, $expiresAt, $createdById]);
return $token;
}
/**
* Valideert een invite token
*/
function validate_invite(string $token): ?array {
$db = get_db_connection();
$stmt = $db->prepare("
SELECT i.*, u.name as inviter_name, u.email as inviter_email
FROM invites i
JOIN users u ON i.created_by_id = u.id
WHERE i.token = ?
");
$stmt->execute([$token]);
$invite = $stmt->fetch();
if (!$invite) return null;
if ($invite['used_at'] !== null) return null;
if (strtotime($invite['expires_at']) < time()) return null;
return $invite;
}
/**
* CSRF token helpers
*/
function get_csrf_token(): string {
start_secure_session();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function verify_csrf_token(?string $token): bool {
start_secure_session();
return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Genereert een cryptografisch ondertekende OAuth state parameter waarin de user_id en timestamp veilig zijn opgeslagen.
* Dit voorkomt CSRF en maakt sessieherstel mogelijk bij cross-site OAuth redirects.
*/
function generate_oauth_state(int $userId): string {
$secret = defined('APP_SECRET') ? APP_SECRET : (defined('DB_PASS') ? DB_PASS : 'fociz_oauth_salt_2026');
$payload = [
'uid' => $userId,
'ts' => time(),
'rnd' => bin2hex(random_bytes(8))
];
$json = json_encode($payload);
$b64 = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
$sig = hash_hmac('sha256', $b64, $secret);
return $b64 . '.' . $sig;
}
/**
* Valideert een ontvangen OAuth state parameter en geeft bij succes het geverifieerde user_id terug.
* Geeft null terug bij ongeldige handtekening of verlopen state (> 30 minuten).
*/
function verify_oauth_state(string $state): ?int {
$secret = defined('APP_SECRET') ? APP_SECRET : (defined('DB_PASS') ? DB_PASS : 'fociz_oauth_salt_2026');
$parts = explode('.', $state, 2);
if (count($parts) !== 2) {
return null;
}
[$b64, $sig] = $parts;
$expectedSig = hash_hmac('sha256', $b64, $secret);
if (!hash_equals($expectedSig, $sig)) {
return null;
}
$remainder = strlen($b64) % 4;
if ($remainder) {
$b64 .= str_repeat('=', 4 - $remainder);
}
$json = base64_decode(strtr($b64, '-_', '+/'));
$payload = json_decode($json, true);
if (!is_array($payload) || empty($payload['uid']) || empty($payload['ts'])) {
return null;
}
// Geldigheidsduur: maximaal 30 minuten (1800 seconden)
if (time() - (int)$payload['ts'] > 1800) {
return null;
}
return (int)$payload['uid'];
}