prepare('SELECT totp_enabled FROM users WHERE id = ?'); $stmt->execute([$userId]); $user = $stmt->fetch(); successResponse([ 'totp_enabled' => (bool) ($user['totp_enabled'] ?? false), ]); } /** POST ?action=setup - vygenerovat secret + QR URI (jeste neaktivuje 2FA) */ function handleSetup(PDO $pdo, TwoFactorAuth $tfa): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { errorResponse('Metoda není povolena', 405); } $authData = JWTAuth::requireAuth(); AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); $userId = $authData['user_id']; $stmt = $pdo->prepare('SELECT totp_enabled, username, email FROM users WHERE id = ?'); $stmt->execute([$userId]); $user = $stmt->fetch(); if ($user['totp_enabled']) { errorResponse('2FA je již aktivní. Nejdříve ji deaktivujte.'); } $secret = $tfa->createSecret(); $stmt = $pdo->prepare('UPDATE users SET totp_secret = ? WHERE id = ?'); $stmt->execute([Encryption::encrypt($secret), $userId]); $label = $user['email'] ?: $user['username']; $qrUri = $tfa->getQRText($label, $secret); successResponse([ 'secret' => $secret, 'qr_uri' => $qrUri, ]); } /** POST ?action=enable { "code": "123456" } */ function handleEnable(PDO $pdo, TwoFactorAuth $tfa): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { errorResponse('Metoda není povolena', 405); } $authData = JWTAuth::requireAuth(); AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); $userId = $authData['user_id']; $input = getJsonInput(); $code = trim($input['code'] ?? ''); if (empty($code)) { errorResponse('Ověřovací kód je povinný'); } $stmt = $pdo->prepare('SELECT totp_secret, totp_enabled FROM users WHERE id = ?'); $stmt->execute([$userId]); $user = $stmt->fetch(); if (!$user['totp_secret']) { errorResponse('Nejprve vygenerujte tajný klíč (setup)'); } if ($user['totp_enabled']) { errorResponse('2FA je již aktivní'); } $decryptedSecret = decryptTotpSecret($user['totp_secret']); if (!$tfa->verifyCode($decryptedSecret, $code)) { errorResponse('Neplatný ověřovací kód. Zkontrolujte čas na telefonu.'); } $backupCodes = generateBackupCodes(); $hashedCodes = array_map(fn ($c) => password_hash($c, PASSWORD_BCRYPT, ['cost' => 10]), $backupCodes); $stmt = $pdo->prepare('UPDATE users SET totp_enabled = 1, totp_backup_codes = ? WHERE id = ?'); $stmt->execute([json_encode($hashedCodes), $userId]); AuditLog::logUpdate('user', $userId, ['totp_enabled' => 0], ['totp_enabled' => 1], 'Uživatel aktivoval 2FA'); successResponse([ 'backup_codes' => $backupCodes, ], '2FA bylo úspěšně aktivováno'); } /** POST ?action=disable { "code": "123456" } */ function handleDisable(PDO $pdo, TwoFactorAuth $tfa): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { errorResponse('Metoda není povolena', 405); } $authData = JWTAuth::requireAuth(); AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); $userId = $authData['user_id']; $input = getJsonInput(); $code = trim($input['code'] ?? ''); if (empty($code)) { errorResponse('Ověřovací kód je povinný'); } $stmt = $pdo->prepare('SELECT totp_secret, totp_enabled FROM users WHERE id = ?'); $stmt->execute([$userId]); $user = $stmt->fetch(); if (!$user['totp_enabled']) { errorResponse('2FA není aktivní'); } $decryptedSecret = decryptTotpSecret($user['totp_secret']); if (!$tfa->verifyCode($decryptedSecret, $code)) { errorResponse('Neplatný ověřovací kód'); } $stmt = $pdo->prepare( 'UPDATE users SET totp_enabled = 0, totp_secret = NULL, totp_backup_codes = NULL WHERE id = ?' ); $stmt->execute([$userId]); AuditLog::logUpdate('user', $userId, ['totp_enabled' => 1], ['totp_enabled' => 0], 'Uživatel deaktivoval 2FA'); successResponse(null, '2FA bylo deaktivováno'); } /** * POST ?action=verify - overeni TOTP kodu pri loginu (pre-auth) * Body: { "login_token": "...", "code": "123456", "remember": false } */ function handleVerify(PDO $pdo, TwoFactorAuth $tfa): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { errorResponse('Metoda není povolena', 405); } $rateLimiter = new RateLimiter(); $rateLimiter->setFailClosed(); $rateLimiter->enforce('totp_2fa', 5); $input = getJsonInput(); $loginToken = $input['login_token'] ?? ''; $code = trim($input['code'] ?? ''); $remember = (bool) ($input['remember'] ?? false); if (empty($loginToken) || empty($code)) { errorResponse('Přihlašovací token a ověřovací kód jsou povinné'); } $tokenData = verifyLoginToken($pdo, $loginToken); if (!$tokenData) { errorResponse('Neplatný nebo expirovaný přihlašovací token. Přihlaste se znovu.', 401); } $userId = $tokenData['user_id']; $stmt = $pdo->prepare(' SELECT u.*, r.name as role_name, r.display_name as role_display_name FROM users u LEFT JOIN roles r ON u.role_id = r.id WHERE u.id = ? AND u.totp_enabled = 1 '); $stmt->execute([$userId]); $user = $stmt->fetch(); if (!$user) { errorResponse('Uživatel nenalezen nebo 2FA není aktivní', 401); } $decryptedSecret = decryptTotpSecret($user['totp_secret']); if (!$tfa->verifyCode($decryptedSecret, $code, 1)) { errorResponse('Neplatný ověřovací kód'); } deleteLoginToken($pdo, $loginToken); completeLogin($pdo, $user, $remember); } /** POST ?action=backup_verify { "login_token": "...", "code": "XXXXXXXX", "remember": false } */ function handleBackupVerify(PDO $pdo): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { errorResponse('Metoda není povolena', 405); } $rateLimiter = new RateLimiter(); $rateLimiter->setFailClosed(); $rateLimiter->enforce('totp_2fa', 5); $input = getJsonInput(); $loginToken = $input['login_token'] ?? ''; $code = strtoupper(trim($input['code'] ?? '')); $remember = (bool) ($input['remember'] ?? false); if (empty($loginToken) || empty($code)) { errorResponse('Přihlašovací token a záložní kód jsou povinné'); } $tokenData = verifyLoginToken($pdo, $loginToken); if (!$tokenData) { errorResponse('Neplatný nebo expirovaný přihlašovací token. Přihlaste se znovu.', 401); } $userId = $tokenData['user_id']; $stmt = $pdo->prepare(' SELECT u.*, r.name as role_name, r.display_name as role_display_name FROM users u LEFT JOIN roles r ON u.role_id = r.id WHERE u.id = ? AND u.totp_enabled = 1 '); $stmt->execute([$userId]); $user = $stmt->fetch(); if (!$user || !$user['totp_backup_codes']) { errorResponse('Uživatel nenalezen nebo nemá záložní kódy', 401); } $hashedCodes = json_decode($user['totp_backup_codes'], true); $matched = false; $remainingCodes = []; foreach ($hashedCodes as $hashed) { if (!$matched && password_verify($code, $hashed)) { $matched = true; } else { $remainingCodes[] = $hashed; } } if (!$matched) { errorResponse('Neplatný záložní kód'); } $stmt = $pdo->prepare('UPDATE users SET totp_backup_codes = ? WHERE id = ?'); $stmt->execute([json_encode($remainingCodes), $userId]); deleteLoginToken($pdo, $loginToken); completeLogin($pdo, $user, $remember); } /** GET ?action=get_required (admin only) */ function handleGetRequired(PDO $pdo): void { $authData = JWTAuth::requireAuth(); AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); requirePermission($authData, 'settings.security'); $stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1"); successResponse([ 'require_2fa' => (bool) $stmt->fetchColumn(), ]); } /** POST ?action=set_required { "required": true/false } (admin only) */ function handleSetRequired(PDO $pdo): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { errorResponse('Metoda není povolena', 405); } $authData = JWTAuth::requireAuth(); AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown'); requirePermission($authData, 'settings.security'); $input = getJsonInput(); $required = (bool) ($input['required'] ?? false); $stmt = $pdo->prepare("UPDATE company_settings SET require_2fa = ? LIMIT 1"); $stmt->execute([$required ? 1 : 0]); successResponse([ 'require_2fa' => $required, ], $required ? '2FA je nyní povinná pro všechny uživatele' : '2FA již není povinná'); } // --- Helper functions --- /** Desifrovat TOTP secret z DB (zpetne kompatibilni s plaintextem pred migraci) */ function decryptTotpSecret(string $value): string { if (Encryption::isEncrypted($value)) { return Encryption::decrypt($value); } return $value; } /** * Generovat 8 nahodnych backup kodu * * @return list */ function generateBackupCodes(int $count = 8): array { $codes = []; for ($i = 0; $i < $count; $i++) { $codes[] = strtoupper(bin2hex(random_bytes(4))); // 8-char hex } return $codes; } /** Docasny login token pro 2FA (5 min) */ function createLoginToken(PDO $pdo, int $userId): string { $token = bin2hex(random_bytes(32)); $hashedToken = hash('sha256', $token); $expiresAt = date('Y-m-d H:i:s', time() + 300); // 5 minutes $stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE user_id = ? OR expires_at < NOW()'); $stmt->execute([$userId]); $stmt = $pdo->prepare(' INSERT INTO totp_login_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?) '); $stmt->execute([$userId, $hashedToken, $expiresAt]); return $token; } /** * Overit login token * * @return array|null */ function verifyLoginToken(PDO $pdo, string $token): ?array { $hashedToken = hash('sha256', $token); $stmt = $pdo->prepare(' SELECT * FROM totp_login_tokens WHERE token_hash = ? AND expires_at > NOW() '); $stmt->execute([$hashedToken]); return $stmt->fetch() ?: null; } /** Smazat login token po pouziti */ function deleteLoginToken(PDO $pdo, string $token): void { $hashedToken = hash('sha256', $token); $stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE token_hash = ?'); $stmt->execute([$hashedToken]); } /** * Dokoncit login po uspesnem 2FA - vydat JWT + refresh token * * @param array $user */ function completeLogin(PDO $pdo, array $user, bool $remember): void { $stmt = $pdo->prepare(' UPDATE users SET failed_login_attempts = 0, locked_until = NULL, last_login = NOW() WHERE id = ? '); $stmt->execute([$user['id']]); $userData = [ 'id' => $user['id'], 'username' => $user['username'], 'email' => $user['email'], 'first_name' => $user['first_name'], 'last_name' => $user['last_name'], 'role' => $user['role_name'] ?? null, 'role_display' => $user['role_display_name'] ?? $user['role_name'] ?? null, 'is_admin' => ($user['role_name'] ?? '') === 'admin', ]; $accessToken = JWTAuth::generateAccessToken($userData); JWTAuth::generateRefreshToken($user['id'], $remember); AuditLog::logLogin($user['id'], $user['username']); $stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1"); $require2FA = (bool) $stmt->fetchColumn(); successResponse([ 'access_token' => $accessToken, 'expires_in' => JWTAuth::getAccessTokenExpiry(), 'user' => [ 'id' => $userData['id'], 'username' => $userData['username'], 'email' => $userData['email'], 'full_name' => trim($userData['first_name'] . ' ' . $userData['last_name']), 'role' => $userData['role'], 'role_display' => $userData['role_display'], 'is_admin' => $userData['is_admin'], 'permissions' => JWTAuth::getUserPermissions($user['id']), 'totp_enabled' => true, 'require_2fa' => $require2FA, ], ], 'Přihlášení úspěšné'); }