115 lines
3.5 KiB
PHP
115 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Budget;
|
|
|
|
use PDO;
|
|
|
|
final class Auth
|
|
{
|
|
public function __construct(private PDO $pdo, private Settings $settings)
|
|
{
|
|
}
|
|
|
|
public function needsSetup(): bool
|
|
{
|
|
return (int) $this->pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() === 0;
|
|
}
|
|
|
|
public function setup(string $email, string $password, bool $enableTwoFactor): ?string
|
|
{
|
|
if (!$this->needsSetup()) {
|
|
throw new \RuntimeException('The administrator account already exists.');
|
|
}
|
|
$secret = $enableTwoFactor ? Totp::generateSecret() : null;
|
|
$statement = $this->pdo->prepare(
|
|
'INSERT INTO users (email, password_hash, totp_secret) VALUES (:email, :password_hash, :secret)'
|
|
);
|
|
$statement->execute([
|
|
'email' => strtolower(trim($email)),
|
|
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
|
'secret' => $secret,
|
|
]);
|
|
$this->settings->set('alerts.email', strtolower(trim($email)));
|
|
$this->settings->set('security.2fa_required', $enableTwoFactor);
|
|
$_SESSION['user_id'] = (int) $this->pdo->lastInsertId();
|
|
session_regenerate_id(true);
|
|
return $secret;
|
|
}
|
|
|
|
public function attempt(string $email, string $password): string
|
|
{
|
|
$statement = $this->pdo->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
|
|
$statement->execute(['email' => strtolower(trim($email))]);
|
|
$user = $statement->fetch();
|
|
if (!$user || !password_verify($password, (string) $user['password_hash'])) {
|
|
return 'invalid';
|
|
}
|
|
|
|
if ((bool) $this->settings->get('security.2fa_required', false)) {
|
|
if (empty($user['totp_secret'])) {
|
|
return 'unconfigured_2fa';
|
|
}
|
|
$_SESSION['pending_2fa_user'] = (int) $user['id'];
|
|
return '2fa';
|
|
}
|
|
|
|
$this->completeLogin((int) $user['id']);
|
|
return 'ok';
|
|
}
|
|
|
|
public function verifyTwoFactor(string $code): bool
|
|
{
|
|
$userId = (int) ($_SESSION['pending_2fa_user'] ?? 0);
|
|
if ($userId === 0) {
|
|
return false;
|
|
}
|
|
$statement = $this->pdo->prepare('SELECT totp_secret FROM users WHERE id = :id');
|
|
$statement->execute(['id' => $userId]);
|
|
$secret = (string) $statement->fetchColumn();
|
|
if ($secret === '' || !Totp::verify($secret, $code)) {
|
|
return false;
|
|
}
|
|
unset($_SESSION['pending_2fa_user']);
|
|
$this->completeLogin($userId);
|
|
return true;
|
|
}
|
|
|
|
public function check(): bool
|
|
{
|
|
return isset($_SESSION['user_id']);
|
|
}
|
|
|
|
public function id(): ?int
|
|
{
|
|
return $this->check() ? (int) $_SESSION['user_id'] : null;
|
|
}
|
|
|
|
public function user(): ?array
|
|
{
|
|
if (!$this->check()) {
|
|
return null;
|
|
}
|
|
$statement = $this->pdo->prepare('SELECT id, email, totp_secret, created_at FROM users WHERE id = :id');
|
|
$statement->execute(['id' => $this->id()]);
|
|
return $statement->fetch() ?: null;
|
|
}
|
|
|
|
public function logout(): void
|
|
{
|
|
$_SESSION = [];
|
|
if (ini_get('session.use_cookies')) {
|
|
$parameters = session_get_cookie_params();
|
|
setcookie(session_name(), '', time() - 42000, $parameters['path'], $parameters['domain'], $parameters['secure'], $parameters['httponly']);
|
|
}
|
|
session_destroy();
|
|
}
|
|
|
|
private function completeLogin(int $userId): void
|
|
{
|
|
$_SESSION['user_id'] = $userId;
|
|
session_regenerate_id(true);
|
|
}
|
|
}
|