Files
FileRise/totp_recover.php
2025-04-11 10:44:26 -04:00

115 lines
3.8 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// totp_recover.php
require_once 'config.php';
header('Content-Type: application/json');
// ——— 1) Only POST ———
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
error_log("Recovery attempt with invalid method: {$_SERVER['REQUEST_METHOD']}");
exit(json_encode(['status'=>'error','message'=>'Method not allowed']));
}
// ——— 2) CSRF check ———
if (empty($_SERVER['HTTP_X_CSRF_TOKEN'])
|| $_SERVER['HTTP_X_CSRF_TOKEN'] !== ($_SESSION['csrf_token'] ?? '')) {
http_response_code(403);
error_log("Invalid CSRF token on recovery for IP {$_SERVER['REMOTE_ADDR']}");
exit(json_encode(['status'=>'error','message'=>'Invalid CSRF token']));
}
// ——— 3) Identify user to recover ———
$userId = $_SESSION['username']
?? $_SESSION['pending_login_user']
?? null;
if (!$userId) {
http_response_code(401);
error_log("Unauthorized recovery attempt from IP {$_SERVER['REMOTE_ADDR']}");
exit(json_encode(['status'=>'error','message'=>'Unauthorized']));
}
// ——— Validate userId format ———
if (!preg_match(REGEX_USER, $userId)) {
http_response_code(400);
error_log("Invalid userId format: {$userId}");
exit(json_encode(['status'=>'error','message'=>'Invalid user identifier']));
}
// ——— Ratelimit recovery attempts ———
$attemptsFile = rtrim(USERS_DIR, '/\\') . '/recovery_attempts.json';
$attempts = is_file($attemptsFile)
? json_decode(file_get_contents($attemptsFile), true)
: [];
$key = $_SERVER['REMOTE_ADDR'] . '|' . $userId;
$now = time();
// Prune >15min old
if (isset($attempts[$key])) {
$attempts[$key] = array_filter(
$attempts[$key],
fn($ts) => $ts > $now - 900
);
}
if (count($attempts[$key] ?? []) >= 5) {
http_response_code(429);
exit(json_encode(['status'=>'error','message'=>'Too many attempts. Try again later.']));
}
// ——— 4) Load user metadata file ———
$userFile = rtrim(USERS_DIR, '/\\') . DIRECTORY_SEPARATOR . $userId . '.json';
if (!file_exists($userFile)) {
http_response_code(404);
error_log("User file not found for recovery: {$userFile}");
exit(json_encode(['status'=>'error','message'=>'User not found']));
}
// ——— 5) Read & lock file ———
$fp = fopen($userFile, 'c+');
if (!$fp || !flock($fp, LOCK_EX)) {
http_response_code(500);
error_log("Failed to lock user file: {$userFile}");
exit(json_encode(['status'=>'error','message'=>'Server error']));
}
$data = json_decode(stream_get_contents($fp), true) ?: [];
// ——— 6) Verify recovery code ———
$input = json_decode(file_get_contents('php://input'), true)['recovery_code'] ?? '';
if (!$input) {
flock($fp, LOCK_UN);
fclose($fp);
http_response_code(400);
exit(json_encode(['status'=>'error','message'=>'Recovery code required']));
}
$hash = $data['totp_recovery_code'] ?? null;
if (!$hash || !password_verify($input, $hash)) {
// record failed attempt
$attempts[$key][] = $now;
file_put_contents($attemptsFile, json_encode($attempts), LOCK_EX);
flock($fp, LOCK_UN);
fclose($fp);
error_log("Invalid recovery code for user {$userId} from IP {$_SERVER['REMOTE_ADDR']}");
exit(json_encode(['status'=>'error','message'=>'Invalid recovery code']));
}
// ——— 7) Invalidate code & save ———
$data['totp_recovery_code'] = null;
rewind($fp);
ftruncate($fp, 0);
fwrite($fp, json_encode($data)); // no pretty-print in prod
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
// ——— 8) Finalize login ———
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $userId;
unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']);
// ——— 9) Success ———
echo json_encode(['status'=>'ok']);
exit;