-
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user