api/two_factor.php
<?php
declare(strict_types=1);
/*
|--------------------------------------------------------------------------
| Two-factor authentication endpoint
|--------------------------------------------------------------------------
|
| Authenticated-only management endpoint used by the settings modal.
| It never calls an external provider; it only generates a local TOTP secret,
| verifies the first code, and stores one-way hashes of backup codes.
|
*/
require_once __DIR__ . '/../includes/core.php';
require_once __DIR__ . '/../includes/modules/auth.php';
loadTranslations();
brivaciaSecurityHeaders();
header('Content-Type: application/json; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
function brivacia_api_two_factor_response(array $payload, int $status = 200): never
{
http_response_code($status);
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
function brivacia_api_two_factor_require_post(): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Method not allowed.',
], 405);
}
}
function brivacia_api_two_factor_json(): array
{
$raw = file_get_contents('php://input');
if ($raw === false || trim($raw) === '') {
return $_POST;
}
$data = json_decode($raw, true);
if (!is_array($data)) {
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Invalid JSON payload.',
], 400);
}
return $data;
}
try {
brivacia_api_two_factor_require_post();
brivaciaRequireApiAuth();
brivaciaRequireCsrf();
$action = (string)($_GET['action'] ?? '');
if ($action === 'setup') {
if (brivaciaTwoFactorEnabled()) {
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Two-factor authentication is already enabled.',
], 409);
}
$setup = brivaciaTwoFactorCreateSetup();
brivacia_api_two_factor_response([
'ok' => true,
'setup_token' => $setup['token'],
'qr_svg' => brivaciaQrSvg($setup['otpauth_uri']),
]);
}
if ($action === 'manual') {
if (brivaciaTwoFactorEnabled()) {
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Two-factor authentication is already enabled.',
], 409);
}
$input = brivacia_api_two_factor_json();
$secret = brivaciaTwoFactorPendingSecret((string)($input['setup_token'] ?? ''));
if ($secret === '') {
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Invalid or expired 2FA setup.',
], 422);
}
brivacia_api_two_factor_response([
'ok' => true,
'secret_display' => brivaciaTwoFactorFormatSecret($secret),
'otpauth_uri' => brivaciaTwoFactorOtpauthUri($secret),
]);
}
if ($action === 'enable') {
if (brivaciaTwoFactorEnabled()) {
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Two-factor authentication is already enabled.',
], 409);
}
$input = brivacia_api_two_factor_json();
$secret = brivaciaTwoFactorPendingSecret((string)($input['setup_token'] ?? ''));
$code = (string)($input['code'] ?? '');
$matchedCounter = null;
if ($secret === '') {
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Invalid or expired 2FA setup.',
], 422);
}
if (!brivaciaTwoFactorVerifyTotp($secret, $code, time(), 1, $matchedCounter)) {
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Invalid verification code.',
], 422);
}
$backupCodes = brivaciaTwoFactorGenerateBackupCodes(10);
brivaciaTwoFactorEnable($secret, $backupCodes, (int)($matchedCounter ?? 0));
brivacia_api_two_factor_response([
'ok' => true,
'enabled' => true,
'backup_codes' => $backupCodes,
]);
}
if ($action === 'disable') {
$input = brivacia_api_two_factor_json();
$password = (string)($input['password'] ?? '');
// Disabling 2FA is a security-downgrading action, so it requires
// re-confirming the password at this exact moment — not just a
// valid session cookie, which could be stolen/hijacked without the
// attacker ever knowing the password.
if (!password_verify($password, brivaciaAuthPasswordHash())) {
brivacia_api_two_factor_response([
'ok' => false,
'error' => t('settings.two_factor.disable.wrong_password'),
], 401);
}
brivaciaTwoFactorDisable();
brivacia_api_two_factor_response([
'ok' => true,
'enabled' => false,
]);
}
brivacia_api_two_factor_response([
'ok' => false,
'error' => 'Unknown action.',
], 400);
} catch (Throwable $e) {
brivacia_api_two_factor_response([
'ok' => false,
'error' => $e->getMessage(),
], 500);
}