This commit is contained in:
@@ -192,7 +192,11 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
|
||||
<!-- Success message will go here -->
|
||||
</div>
|
||||
<div id="qrcode"></div>
|
||||
<div class="p-3">
|
||||
<input type="text" id="twofa-confirm-pin" class="form-control" placeholder="Current 2FA code">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="confirm2FaEnrollment()">Confirm 2FA</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,8 +366,12 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
|
||||
// Handle success
|
||||
if(isEnabled==false){
|
||||
showSuccessModal(result.message || (isEnabled ? '2FA enabled successfully.' : '2FA disabled successfully.'));
|
||||
}else{
|
||||
show2FaModal(result.message, result.token);
|
||||
}else{
|
||||
if (result.pending) {
|
||||
show2FaModal(result.message, result.token);
|
||||
} else {
|
||||
showSuccessModal(result.message || '2FA enabled successfully.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle error
|
||||
@@ -427,11 +435,41 @@ if (!isset($_SESSION["logged_in"]) || $_SESSION["logged_in"] !== true) {
|
||||
}
|
||||
function show2FaModal(message,secret) {
|
||||
document.getElementById('twofaModalMessage').textContent = message;
|
||||
document.getElementById('qrcode').innerHTML = '';
|
||||
document.getElementById('twofa-confirm-pin').value = '';
|
||||
const errorModal = new bootstrap.Modal(document.getElementById('twofaModal'));
|
||||
generate2FAQRCode("Jakach Login",'<?php echo($_SESSION["username"]) ?>',secret);
|
||||
errorModal.show();
|
||||
}
|
||||
|
||||
async function confirm2FaEnrollment() {
|
||||
const pin = document.getElementById('twofa-confirm-pin').value.trim();
|
||||
if (!pin) {
|
||||
showErrorModal('Enter the current 2FA code.');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/account/update_2fa.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enable_2fa: true,
|
||||
twofa_pin: pin
|
||||
}),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok && result.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('twofaModal')).hide();
|
||||
document.getElementById('2fa-switch').checked = true;
|
||||
showSuccessModal(result.message || '2FA enabled successfully.');
|
||||
} else {
|
||||
showErrorModal(result.message || 'Invalid 2FA code.');
|
||||
}
|
||||
}
|
||||
|
||||
function generate2FAQRCode(issuer, accountName, secret) {
|
||||
// Create the OTP URI
|
||||
const uri = `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}`;
|
||||
|
||||
@@ -39,13 +39,28 @@ if(!isset($data->enable_2fa) || !is_bool($data->enable_2fa)){
|
||||
exit();
|
||||
}
|
||||
if($data->enable_2fa==true){
|
||||
//create 2fa secret key
|
||||
$twofa_secret=generateBase32Secret();
|
||||
$twofa_pin = trim((string)($data->twofa_pin ?? ""));
|
||||
if ($twofa_pin === "") {
|
||||
$twofa_secret=generateBase32Secret();
|
||||
$_SESSION["pending_2fa_secret"]=$twofa_secret;
|
||||
echo json_encode(['success' => true, 'pending' => true, 'message' => 'Scan this QR code, then enter the current 2FA code to confirm enrollment.', 'token' => $twofa_secret]);
|
||||
exit();
|
||||
}
|
||||
|
||||
check_rate_limit($conn, 'setup_2fa', 5, 10 * 60, (string)$id);
|
||||
$twofa_secret = $_SESSION["pending_2fa_secret"] ?? "";
|
||||
if ($twofa_secret === "" || !hash_equals(generateTOTP($twofa_secret), $twofa_pin)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid 2FA code.']);
|
||||
exit();
|
||||
}
|
||||
|
||||
$sql="UPDATE users SET 2fa = ?, auth_method_enabled_2fa = 1, auth_method_required_2fa = 1 WHERE id = ?";
|
||||
if ($update_stmt = $conn->prepare($sql)) {
|
||||
$update_stmt->bind_param("si", $twofa_secret, $id);
|
||||
if ($update_stmt->execute()) {
|
||||
echo json_encode(['success' => true, 'message' => '2FA enabled. Your 2fa secret is: '.$twofa_secret.'', 'token' => $twofa_secret]);
|
||||
unset($_SESSION["pending_2fa_secret"]);
|
||||
clear_rate_limit($conn, 'setup_2fa', (string)$id);
|
||||
echo json_encode(['success' => true, 'message' => '2FA enabled.']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Failed to enable 2fa.']);
|
||||
}
|
||||
@@ -56,8 +71,8 @@ if($data->enable_2fa==true){
|
||||
}
|
||||
|
||||
if($data->enable_2fa==false){
|
||||
//create 2fa secret key
|
||||
$sql="UPDATE users SET auth_method_enabled_2fa = 0, auth_method_required_2fa = 0 WHERE id = ?";
|
||||
unset($_SESSION["pending_2fa_secret"]);
|
||||
$sql="UPDATE users SET 2fa = '', auth_method_enabled_2fa = 0, auth_method_required_2fa = 0 WHERE id = ?";
|
||||
if ($update_stmt = $conn->prepare($sql)) {
|
||||
$update_stmt->bind_param("i",$id);
|
||||
if ($update_stmt->execute()) {
|
||||
|
||||
@@ -30,15 +30,19 @@ try {
|
||||
|
||||
// read get argument and post body
|
||||
$fn = filter_input(INPUT_GET, 'fn');
|
||||
if (!in_array($fn, ['getCreateArgs', 'processCreate'], true)) {
|
||||
throw new Exception('Invalid passkey operation.');
|
||||
}
|
||||
$requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey');
|
||||
$userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS);
|
||||
|
||||
$userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS);
|
||||
$userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS);
|
||||
$userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS);
|
||||
$userVerification = 'preferred';
|
||||
|
||||
$userName = preg_replace('/[^0-9a-z_]/i', '', $_SESSION["username"] ?? "");
|
||||
if ($userName === "") {
|
||||
throw new Exception('Missing account session.');
|
||||
}
|
||||
$userId = bin2hex($userName);
|
||||
$userDisplayName = $userName;
|
||||
$userId = preg_replace('/[^0-9a-f]/i', '', $userId);
|
||||
$userName = preg_replace('/[^0-9a-z]/i', '', $_SESSION["username"]);
|
||||
$userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName);
|
||||
|
||||
$post = trim(file_get_contents('php://input'));
|
||||
@@ -96,6 +100,7 @@ try {
|
||||
|
||||
// Handle different functions
|
||||
if ($fn === 'getCreateArgs') {
|
||||
check_rate_limit($conn, 'passkey_register_args', 10, 60 * 60, $userName);
|
||||
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
@@ -103,6 +108,7 @@ try {
|
||||
|
||||
// save challange to session. you have to deliver it to processGet later.
|
||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||
$_SESSION['passkey_register_username'] = $userName;
|
||||
|
||||
} else if ($fn === 'getGetArgs') {
|
||||
$ids = [];
|
||||
@@ -137,6 +143,15 @@ try {
|
||||
// save challange to session. you have to deliver it to processGet later.
|
||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||
} else if ($fn === 'processCreate') {
|
||||
check_rate_limit($conn, 'passkey_register_process', 5, 60 * 60, $userName);
|
||||
if (empty($_SESSION['challenge']) || ($_SESSION['passkey_register_username'] ?? '') !== $userName) {
|
||||
throw new Exception('Invalid passkey session.');
|
||||
}
|
||||
foreach (['clientDataJSON', 'attestationObject'] as $requiredField) {
|
||||
if (!is_object($post) || empty($post->{$requiredField})) {
|
||||
throw new Exception('Invalid passkey response.');
|
||||
}
|
||||
}
|
||||
// Process create
|
||||
$challenge = $_SESSION['challenge'];
|
||||
$clientDataJSON = base64_decode($post->clientDataJSON);
|
||||
@@ -151,8 +166,15 @@ try {
|
||||
$data->userDisplayName = $userDisplayName;
|
||||
|
||||
// Store registration data in the database
|
||||
$credentialId = $data->credentialId;
|
||||
$credentialPublicKey = $data->credentialPublicKey;
|
||||
$signatureCounter = (int)$data->signatureCounter;
|
||||
$stmt = $conn->prepare("UPDATE users set credential_id = ?, public_key = ?, counter = ?, auth_method_enabled_passkey = 1, auth_method_required_passkey = 1 WHERE username = ?");
|
||||
$stmt->execute([ $data->credentialId, $data->credentialPublicKey, $data->signatureCounter,$userName]);
|
||||
$stmt->bind_param("ssis", $credentialId, $credentialPublicKey, $signatureCounter, $userName);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
unset($_SESSION['challenge'], $_SESSION['passkey_register_username']);
|
||||
clear_rate_limit($conn, 'passkey_register_process', $userName);
|
||||
|
||||
$msg = 'registration success.';
|
||||
$return = new stdClass();
|
||||
@@ -165,7 +187,7 @@ try {
|
||||
} catch (Throwable $ex) {
|
||||
$return = new stdClass();
|
||||
$return->success = false;
|
||||
$return->msg = $ex->getMessage();
|
||||
$return->msg = 'Passkey registration failed.';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
print(json_encode($return));
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include "../utils/security.php";
|
||||
include "../../config/config.php";
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
|
||||
check_rate_limit($conn, 'check_auth_key', 120, 60);
|
||||
$now=time();
|
||||
$sql="DELETE FROM auth_tokens WHERE valid_until < ?;";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
@@ -10,9 +12,10 @@ mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
|
||||
$auth_key=$_GET["auth_token"] ?? "";
|
||||
$auth_key_hash=auth_token_hash($auth_key);
|
||||
$sql="SELECT user_id FROM auth_tokens WHERE auth_token = ? AND valid_until > ?;";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'si', $auth_key,$now);
|
||||
mysqli_stmt_bind_param($stmt, 'si', $auth_key_hash,$now);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_store_result($stmt);
|
||||
//if auth key is valid
|
||||
@@ -48,7 +51,7 @@ if(mysqli_stmt_num_rows($stmt) == 1){
|
||||
//remove auth key
|
||||
$sql="DELETE FROM auth_tokens WHERE auth_token = ?;";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 's', $auth_key);
|
||||
mysqli_stmt_bind_param($stmt, 's', $auth_key_hash);
|
||||
mysqli_stmt_execute($stmt);
|
||||
echo(json_encode($data));
|
||||
}else{
|
||||
|
||||
@@ -14,6 +14,7 @@ include "../utils/generate_pin.php";
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
|
||||
|
||||
$username=$_SESSION["username"];
|
||||
check_rate_limit($conn, 'login_mfa', 5, 10 * 60, $username);
|
||||
$sql="SELECT 2fa FROM users WHERE username = ?";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 's', $username);
|
||||
@@ -28,6 +29,7 @@ $twofa_pin=$_POST["twofa_pin"] ?? "";
|
||||
if($twofa_secret !== "" && hash_equals(generateTOTP($twofa_secret), $twofa_pin)){
|
||||
$_SESSION["mfa_authenticated"]=1;
|
||||
session_regenerate_id(true);
|
||||
clear_rate_limit($conn, 'login_mfa', $username);
|
||||
$data = [
|
||||
'status' => 'success'
|
||||
];
|
||||
|
||||
@@ -15,15 +15,19 @@ if ($conn->connect_error) {
|
||||
try {
|
||||
// read get argument and post body
|
||||
$fn = filter_input(INPUT_GET, 'fn');
|
||||
if (!in_array($fn, ['getGetArgs', 'processGet'], true)) {
|
||||
throw new Exception('Invalid passkey operation.');
|
||||
}
|
||||
$requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey');
|
||||
$userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS);
|
||||
|
||||
$userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS);
|
||||
$userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS);
|
||||
$userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS);
|
||||
$userVerification = 'preferred';
|
||||
|
||||
$userName = preg_replace('/[^0-9a-z_]/i', '', $_SESSION["username"] ?? "");
|
||||
if ($userName === "") {
|
||||
throw new Exception('Missing login session.');
|
||||
}
|
||||
$userId = bin2hex($userName);
|
||||
$userDisplayName = $userName;
|
||||
$userId = preg_replace('/[^0-9a-f]/i', '', $userId);
|
||||
$userName = preg_replace('/[^0-9a-z]/i', '', $userName);
|
||||
$userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName);
|
||||
|
||||
$post = trim(file_get_contents('php://input'));
|
||||
@@ -88,6 +92,7 @@ try {
|
||||
|
||||
// Save challenge to session or somewhere else if needed
|
||||
} else if ($fn === 'getGetArgs') {
|
||||
check_rate_limit($conn, 'passkey_get_args', 10, 5 * 60, $userName);
|
||||
$ids = [];
|
||||
|
||||
//get registrations form user table
|
||||
@@ -100,11 +105,12 @@ try {
|
||||
$row = $registration->fetch_assoc();
|
||||
|
||||
|
||||
if ($registration->num_rows <= 0) {
|
||||
if ($registration->num_rows <= 0 || empty($row["credential_id"])) {
|
||||
throw new Exception('User does not exist');
|
||||
}
|
||||
|
||||
$_SESSION["registrations"]["credentialId"]=$row["credential_id"];
|
||||
$_SESSION["registrations"]["username"]=$userName;
|
||||
|
||||
$ids[]=$row["credential_id"];
|
||||
$_SESSION["registrations"]["userId"]=$userId;
|
||||
@@ -118,16 +124,28 @@ try {
|
||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||
|
||||
}else if ($fn === 'processGet') {
|
||||
check_rate_limit($conn, 'passkey_process', 5, 10 * 60, $userName);
|
||||
if (empty($_SESSION["challenge"]) || empty($_SESSION["registrations"]["credentialId"]) || ($_SESSION["registrations"]["username"] ?? "") !== $userName) {
|
||||
throw new Exception('Invalid passkey session.');
|
||||
}
|
||||
foreach (['id', 'clientDataJSON', 'authenticatorData', 'signature'] as $requiredField) {
|
||||
if (!is_object($post) || empty($post->{$requiredField})) {
|
||||
throw new Exception('Invalid passkey response.');
|
||||
}
|
||||
}
|
||||
// Process get
|
||||
// Retrieve registration data from the database based on credential ID
|
||||
$id = base64_decode($post->id);
|
||||
$stmt = $conn->prepare("SELECT * FROM users WHERE credential_id = ?");
|
||||
$stmt->bind_param("s", $_SESSION["registrations"]["credentialId"]);
|
||||
if (!hash_equals($_SESSION["registrations"]["credentialId"], $id)) {
|
||||
throw new Exception('Invalid credential.');
|
||||
}
|
||||
$stmt = $conn->prepare("SELECT public_key, counter FROM users WHERE credential_id = ? AND username = ?");
|
||||
$stmt->bind_param("ss", $_SESSION["registrations"]["credentialId"], $userName);
|
||||
$stmt->execute();
|
||||
$registration = $stmt->get_result();
|
||||
$row = $registration->fetch_assoc();
|
||||
|
||||
if (!$registration) {
|
||||
if (!$row) {
|
||||
throw new Exception('Public Key for credential ID not found!');
|
||||
}
|
||||
|
||||
@@ -139,7 +157,14 @@ try {
|
||||
$credentialPublicKey = $row['public_key'];
|
||||
|
||||
// Process the get request
|
||||
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required');
|
||||
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, (int)$row['counter'], $userVerification === 'required');
|
||||
$newCounter = $WebAuthn->getSignatureCounter();
|
||||
if ($newCounter !== null) {
|
||||
$stmt = $conn->prepare("UPDATE users SET counter = ? WHERE username = ?");
|
||||
$stmt->bind_param("is", $newCounter, $userName);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
// Authentication success
|
||||
//set sessionso user is authenticated
|
||||
@@ -147,6 +172,8 @@ try {
|
||||
$_SESSION["pw_authenticated"]=1;
|
||||
$_SESSION["passkey_authenticated"]=1;
|
||||
session_regenerate_id(true);
|
||||
unset($_SESSION["challenge"], $_SESSION["registrations"]);
|
||||
clear_rate_limit($conn, 'passkey_process', $userName);
|
||||
$return = new stdClass();
|
||||
$return->success = true;
|
||||
|
||||
@@ -158,7 +185,7 @@ try {
|
||||
} catch (Throwable $ex) {
|
||||
$return = new stdClass();
|
||||
$return->success = false;
|
||||
$return->msg = $ex->getMessage();
|
||||
$return->msg = 'Passkey authentication failed.';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
print(json_encode($return));
|
||||
|
||||
@@ -13,6 +13,7 @@ include "../../config/config.php";
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
|
||||
|
||||
$username=$_SESSION["username"];
|
||||
check_rate_limit($conn, 'login_pw', 5, 15 * 60, $username);
|
||||
$sql="SELECT password,pepper FROM users WHERE username = ?";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 's', $username);
|
||||
@@ -28,6 +29,7 @@ $password=$_POST["password"] ?? "";
|
||||
if($pw !== "" && password_verify($password.$pepper,$pw)){
|
||||
$_SESSION["pw_authenticated"]=1;
|
||||
session_regenerate_id(true);
|
||||
clear_rate_limit($conn, 'login_pw', $username);
|
||||
$data = [
|
||||
'status' => 'success'
|
||||
];
|
||||
|
||||
@@ -49,9 +49,10 @@ else if ($_SESSION["needs_auth"]===false && $_SESSION["mfa_authenticated"]==1 &&
|
||||
$user_id=$_SESSION["id"];
|
||||
$valid_until=time()+(15*60);
|
||||
$auth_token=bin2hex(random_bytes(128));
|
||||
$auth_token_hash=auth_token_hash($auth_token);
|
||||
$sql="INSERT INTO auth_tokens (auth_token,user_id, valid_until) VALUES(?,?,?);";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'sii', $auth_token,$user_id,$valid_until);
|
||||
mysqli_stmt_bind_param($stmt, 'sii', $auth_token_hash,$user_id,$valid_until);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
if(!empty($send_to)){
|
||||
|
||||
@@ -15,13 +15,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
|
||||
|
||||
$token = $_POST['token'];
|
||||
$token_hash = auth_token_hash($token);
|
||||
$user_id="";
|
||||
$valid_until=0;
|
||||
$password = $_POST['password'];
|
||||
$confirmPassword = $_POST['confirm_password'];
|
||||
$sql="SELECT user_id, valid_until FROM reset_tokens WHERE auth_token=?;";
|
||||
check_rate_limit($conn, 'reset_pw', 5, 60 * 60, $token_hash);
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 's', $token);
|
||||
mysqli_stmt_bind_param($stmt, 's', $token_hash);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_store_result($stmt);
|
||||
mysqli_stmt_bind_result($stmt, $user_id,$valid_until);
|
||||
@@ -47,6 +49,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($update_stmt = $conn->prepare($update_sql)) {
|
||||
$update_stmt->bind_param("ssi", $hashed_password, $new_pepper, $user_id);
|
||||
if ($update_stmt->execute()) {
|
||||
clear_rate_limit($conn, 'reset_pw', $token_hash);
|
||||
echo json_encode(['status' => 'success','success' => true, 'message' => 'Password updated successfully.']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Failed to update password.']);
|
||||
@@ -62,7 +65,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
//remove token
|
||||
$sql="DELETE FROM reset_tokens WHERE auth_token = ?;";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 's', $token);
|
||||
mysqli_stmt_bind_param($stmt, 's', $token_hash);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
include "../../config/config.php";
|
||||
include "../utils/get_location.php";
|
||||
$username=$_SESSION["username"] ?? "";
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
|
||||
check_rate_limit($conn, 'send_reset_link', 3, 60 * 60, $username);
|
||||
if ($username === "") {
|
||||
echo json_encode(['success' => false, 'message' => 'Missing username.']);
|
||||
exit;
|
||||
}
|
||||
$sql="SELECT id, email, telegram_id FROM users WHERE username = ?;";
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
|
||||
$mail="";
|
||||
$id="";
|
||||
$telegram_id="";
|
||||
@@ -40,6 +41,7 @@ $ip=trim(explode(",",$forwarded_for)[0]);
|
||||
$location=get_location_from_ip($ip);
|
||||
$date=date('Y-m-d H:i:s');
|
||||
$token=bin2hex(random_bytes(128));
|
||||
$token_hash=auth_token_hash($token);
|
||||
$link="https://auth.jakach.ch/login/reset_pw.php?token=$token";
|
||||
|
||||
$message = "*Password reset token*\n\n"
|
||||
@@ -648,7 +650,7 @@ if(!empty($mail)){
|
||||
$valid_until=time()+(12 * 60 * 60);
|
||||
$sql="INSERT INTO reset_tokens (auth_token, user_id,valid_until) VALUES (?,?,?);";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'sii', $token,$id,$valid_until);
|
||||
mysqli_stmt_bind_param($stmt, 'sii', $token_hash,$id,$valid_until);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ require_csrf_token();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_response(['success' => false, 'message' => 'Invalid request method.'], 405);
|
||||
}
|
||||
include "../../config/config.php";
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
|
||||
check_rate_limit($conn, 'set_username', 30, 60);
|
||||
$_SESSION["needs_auth"]=true;
|
||||
$_SESSION["logged_in"]=false;
|
||||
$username = strtolower((string) ($_POST["username"] ?? ""));
|
||||
|
||||
@@ -9,6 +9,7 @@ header('Content-Type: application/json');
|
||||
include "../../config/config.php";
|
||||
// Connect to the database
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD, $DB_DATABASE);
|
||||
check_rate_limit($conn, 'register', 5, 60 * 60);
|
||||
|
||||
// Check the connection
|
||||
if ($conn === false) {
|
||||
|
||||
@@ -99,6 +99,150 @@ function remember_token_hash(string $token): string
|
||||
return hash('sha256', $token);
|
||||
}
|
||||
|
||||
function auth_token_hash(string $token): string
|
||||
{
|
||||
return hash('sha256', $token);
|
||||
}
|
||||
|
||||
function client_ip(): string
|
||||
{
|
||||
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
}
|
||||
|
||||
function ensure_rate_limit_table(mysqli $conn): void
|
||||
{
|
||||
static $created = false;
|
||||
if ($created) {
|
||||
return;
|
||||
}
|
||||
|
||||
$conn->query("CREATE TABLE IF NOT EXISTS rate_limits (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
attempts INT NOT NULL,
|
||||
reset_at INT NOT NULL
|
||||
)");
|
||||
$created = true;
|
||||
}
|
||||
|
||||
function redis_rate_limit_connection()
|
||||
{
|
||||
static $redis = null;
|
||||
static $checked = false;
|
||||
|
||||
if ($checked) {
|
||||
return $redis;
|
||||
}
|
||||
$checked = true;
|
||||
|
||||
if (!class_exists('Redis')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = $GLOBALS['REDIS_HOST'] ?? getenv('REDIS_HOST') ?: null;
|
||||
if (!$host) {
|
||||
$host = 'jakach-login-redis';
|
||||
}
|
||||
$port = $GLOBALS['REDIS_PORT'] ?? getenv('REDIS_PORT') ?: null;
|
||||
$port = $port ? (int)$port : 6379;
|
||||
|
||||
try {
|
||||
$candidate = new Redis();
|
||||
if (!$candidate->connect($host, $port, 0.2)) {
|
||||
return null;
|
||||
}
|
||||
$redis = $candidate;
|
||||
} catch (Throwable $ex) {
|
||||
$redis = null;
|
||||
}
|
||||
|
||||
return $redis;
|
||||
}
|
||||
|
||||
function rate_limit_key(string $bucket, string $identifier = ''): string
|
||||
{
|
||||
return hash('sha256', $bucket . '|' . client_ip() . '|' . strtolower($identifier));
|
||||
}
|
||||
|
||||
function check_rate_limit(mysqli $conn, string $bucket, int $max_attempts, int $window_seconds, string $identifier = ''): void
|
||||
{
|
||||
$redis = redis_rate_limit_connection();
|
||||
if ($redis !== null) {
|
||||
$key = 'rl:' . rate_limit_key($bucket, $identifier);
|
||||
$attempts = (int) $redis->incr($key);
|
||||
if ($attempts === 1) {
|
||||
$redis->expire($key, $window_seconds);
|
||||
}
|
||||
if ($attempts > $max_attempts) {
|
||||
json_response([
|
||||
'success' => false,
|
||||
'status' => 'failure',
|
||||
'message' => 'Too many attempts. Please try again later.'
|
||||
], 429);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_rate_limit_table($conn);
|
||||
|
||||
$now = time();
|
||||
$key = rate_limit_key($bucket, $identifier);
|
||||
$attempts = 0;
|
||||
$reset_at = 0;
|
||||
|
||||
$sql = "SELECT attempts, reset_at FROM rate_limits WHERE id = ?";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 's', $key);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_store_result($stmt);
|
||||
mysqli_stmt_bind_result($stmt, $attempts, $reset_at);
|
||||
$found = mysqli_stmt_fetch($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
|
||||
if (!$found || $reset_at <= $now) {
|
||||
$attempts = 1;
|
||||
$reset_at = $now + $window_seconds;
|
||||
$sql = "REPLACE INTO rate_limits (id, attempts, reset_at) VALUES (?, ?, ?)";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'sii', $key, $attempts, $reset_at);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($attempts >= $max_attempts) {
|
||||
json_response([
|
||||
'success' => false,
|
||||
'status' => 'failure',
|
||||
'message' => 'Too many attempts. Please try again later.'
|
||||
], 429);
|
||||
}
|
||||
|
||||
$attempts++;
|
||||
$sql = "UPDATE rate_limits SET attempts = ? WHERE id = ?";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 'is', $attempts, $key);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
}
|
||||
|
||||
function clear_rate_limit(mysqli $conn, string $bucket, string $identifier = ''): void
|
||||
{
|
||||
$redis = redis_rate_limit_connection();
|
||||
if ($redis !== null) {
|
||||
$redis->del('rl:' . rate_limit_key($bucket, $identifier));
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_rate_limit_table($conn);
|
||||
|
||||
$key = rate_limit_key($bucket, $identifier);
|
||||
$sql = "DELETE FROM rate_limits WHERE id = ?";
|
||||
$stmt = mysqli_prepare($conn, $sql);
|
||||
mysqli_stmt_bind_param($stmt, 's', $key);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
}
|
||||
|
||||
function set_secure_cookie(string $name, string $value, int $expires): void
|
||||
{
|
||||
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
$TELEGRAM_BOT_API="";
|
||||
$SENDGRID_KEY="";
|
||||
$SENDGRID_MAIL="";
|
||||
$REDIS_HOST="jakach-login-redis";
|
||||
$REDIS_PORT=6379;
|
||||
?>
|
||||
|
||||
+10
-2
@@ -1,5 +1,3 @@
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
jakach-login-db:
|
||||
image: yobasystems/alpine-mariadb:latest
|
||||
@@ -13,6 +11,15 @@ services:
|
||||
volumes:
|
||||
- jakach-login-db-storage:/var/lib/mysql
|
||||
|
||||
jakach-login-redis:
|
||||
image: redis:7-alpine
|
||||
container_name: jakach-login-redis
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||
networks:
|
||||
jakach-login-network:
|
||||
ipv4_address: 192.168.5.4
|
||||
|
||||
jakach-login-srv:
|
||||
build:
|
||||
context: .
|
||||
@@ -28,6 +35,7 @@ services:
|
||||
- "447:80"
|
||||
depends_on:
|
||||
- jakach-login-db
|
||||
- jakach-login-redis
|
||||
volumes:
|
||||
- ./app-code:/var/www/html
|
||||
- ./apache-conf:/etc/apache2/sites-available
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@ FROM php:apache
|
||||
# Install necessary PHP extensions and tools
|
||||
RUN apt-get update && \
|
||||
apt-get install -y libzip-dev zip zlib1g-dev git unzip && \
|
||||
docker-php-ext-install mysqli zip
|
||||
docker-php-ext-install mysqli zip && \
|
||||
pecl install redis && \
|
||||
docker-php-ext-enable redis
|
||||
|
||||
# Enable SSL module for Apache
|
||||
RUN a2enmod ssl
|
||||
|
||||
Reference in New Issue
Block a user