USER
In our last conversation regarding my procedural PHP project, your latest response was following:
Thank you for providing the complete context of your login system, including the `config.php`, `commonVariables.php`, and `commonFunctions.php` files. This comprehensive view allows for a thorough analysis and targeted recommendations to enhance the security, maintainability, and efficiency of your authentication mechanism.
Below, I will guide you through step-by-step improvements, highlighting potential issues in your current implementation and offering best practices to address them.
---
## **1. Review and Improve `config.php`**
### **Current Configuration:**
```php
<?php
// PHP Set
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
ini_set('memory_limit', '4096M');
ini_set('max_execution_time', 0);
ini_set('mysql.connect_timeout', 0);
ini_set('default_socket_timeout', 0);
set_time_limit(0);
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Strict');
$onWeb = http_response_code() !== FALSE;
if ($onWeb === TRUE) {
$isLive = $_SERVER["SERVER_NAME"] === "valkya.lan" ? FALSE : TRUE;
} else {
$isLive = TRUE;
}
// Database Details
$dbhostname = ($isLive === TRUE ? "localhost:3306" : "localhost");
$dbname = ($isLive === TRUE ? "trackbackemail" : "trackbackemail");
$dbusername = ($isLive === TRUE ? "trackback" : "root");
$dbpassword = ($isLive === TRUE ? "@ksharBrahm1" : "Gunatit@1");
// Create connection
$conn = mysqli_connect($dbhostname, $dbusername, $dbpassword, $dbname);
// Check connection
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
?>
```
### **Issues Identified:**
1. **Error Reporting in Production:**
- **Problem:** Both `display_errors` and `display_startup_errors` are set to `1`, which means all errors will be displayed. This is **not recommended** for production as it can expose sensitive information.
2. **Resource Limits:**
- **Memory Limit:** Set to `4096M` (4GB), which is excessively high and can lead to resource exhaustion.
- **Execution Time:** `max_execution_time` set to `0` (no limit), which can be risky as scripts might hang indefinitely.
- **Sockets:** `mysql.connect_timeout` and `default_socket_timeout` set to `0`, causing no timeout. This can lead to hanging connections.
3. **Session Cookie Configuration:**
- **Problem:** Setting session cookies via `ini_set` is acceptable, but these settings should be enforced **before** `session_start()`.
- **`session.cookie_samesite`:** The `samesite` attribute isn't directly supported via `ini_set` in older PHP versions.
4. **Environment Detection:**
- **Problem:** The detection of live vs. development is based on the server name `valkya.lan`. This is fragile and can lead to misconfiguration.
5. **Database Credentials:**
- **Problem:** Credentials are hard-coded, which is a security risk, especially if the codebase is version-controlled or accessible.
### **Recommendations:**
1. **Separate Environments:**
- Manage configurations for different environments (development, staging, production) separately to prevent accidental exposure of sensitive settings.
2. **Error Reporting:**
- **Development:**
```php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
```
- **Production:**
```php
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
error_reporting(E_ALL);
```
- **Implementation:**
```php
if ($isLive === TRUE) {
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
} else {
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
}
error_reporting(E_ALL);
```
3. **Resource Limits:**
- **Production:**
```php
ini_set('memory_limit', '256M'); // Adjust as needed
ini_set('max_execution_time', 30); // 30 seconds
ini_set('mysql.connect_timeout', 10); // 10 seconds
ini_set('default_socket_timeout', 10); // 10 seconds
set_time_limit(30); // 30 seconds
```
- **Explanation:** These settings help prevent resource exhaustion and ensure scripts don't hang indefinitely.
4. **Session Security:**
- **Move Session Cookie Settings Before `session_start()`:**
```php
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Strict');
session_start();
```
- **Alternative (Using `session_set_cookie_params`):**
```php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'yourdomain.com', // Update accordingly
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
]);
session_start();
```
- **Note:** The `samesite` attribute handling can vary based on PHP versions. Using `session_set_cookie_params` is more reliable.
5. **Environment Configuration:**
- **Use Environment Variables:**
Instead of hard-coding environment detection based on server name, consider using environment variables or separate configuration files for each environment.
**Example:**
```php
$environment = getenv('APP_ENV') ?: 'development'; // 'production', 'staging', 'development'
if ($environment === 'production') {
$isLive = TRUE;
// Production-specific configurations
} else {
$isLive = FALSE;
// Development-specific configurations
}
```
6. **Secure Database Credentials:**
- **Use Environment Variables:**
Store sensitive information like database credentials in environment variables instead of hard-coding them.
**Example:**
```php
$dbhostname = getenv('DB_HOST') ?: 'localhost';
$dbname = getenv('DB_NAME') ?: 'trackbackemail';
$dbusername = getenv('DB_USER') ?: 'root';
$dbpassword = getenv('DB_PASS') ?: '';
```
- **Set Environment Variables Securely:**
- **On Server:** Use server configurations or tools like [dotenv](https://github.com/vlucas/phpdotenv) to manage environment variables.
- **Do Not** commit environment files containing sensitive data to version control.
7. **Error Handling:**
- **Avoid using `die()` in Production:**
Instead of stopping script execution abruptly, implement proper error handling and user-friendly error messages.
**Example:**
```php
if (!$conn) {
error_log("Database Connection Failed: " . mysqli_connect_error());
// Redirect to an error page or show a user-friendly message
header('Location: /error.php');
exit;
}
```
### **Revised `config.php`:**
```php
<?php
// Load environment variables (Use composer dotenv or similar in production)
// For example, using Dotenv
// require_once 'vendor/autoload.php';
// $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
// $dotenv->load();
// Determine the environment
// You should set APP_ENV to 'production' in your server's environment
$environment = getenv('APP_ENV') ?: 'development';
$isLive = ($environment === 'production') ? TRUE : FALSE;
// Error Reporting
if ($isLive === TRUE) {
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
} else {
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
}
error_reporting(E_ALL);
// Resource Limits
if ($isLive === TRUE) {
ini_set('memory_limit', '256M'); // Adjust as needed
ini_set('max_execution_time', 30); // 30 seconds
ini_set('mysql.connect_timeout', 10); // 10 seconds
ini_set('default_socket_timeout', 10); // 10 seconds
set_time_limit(30); // 30 seconds
} else {
// Development-specific resource limits
ini_set('memory_limit', '4096M');
ini_set('max_execution_time', 0);
ini_set('mysql.connect_timeout', 0);
ini_set('default_socket_timeout', 0);
set_time_limit(0);
}
// Session Cookie Settings
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => ($isLive) ? 'trackback.email' : 'valkya.lan',
'secure' => true, // Ensure HTTPS
'httponly' => true,
'samesite' => 'Strict', // Adjust as per requirement
]);
session_start();
// Database Details (Use environment variables)
$dbhostname = getenv('DB_HOST') ?: 'localhost';
$dbname = getenv('DB_NAME') ?: 'trackbackemail';
$dbusername = getenv('DB_USER') ?: 'root';
$dbpassword = getenv('DB_PASS') ?: '';
// Create connection
$conn = mysqli_connect($dbhostname, $dbusername, $dbpassword, $dbname);
// Check connection
if (!$conn) {
error_log("Database Connection Failed: " . mysqli_connect_error());
// Redirect or handle the error appropriately
header('Location: /error.php');
exit;
}
?>
```
### **Notes:**
- **Environment Variables:** Utilize tools like [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv) to manage environment variables securely.
- **Error Pages:** Create a generic `error.php` to display user-friendly error messages without exposing sensitive information.
---
## **2. Review and Enhance `commonVariables.php`**
### **Current Configuration:**
```php
<?php
// Global constants
define('SMTP_HOST', 'smtp.ionos.com');
define('SMTP_USERNAME', 'no-reply@trackback.email');
define('SMTP_PASSWORD', 'Gunatit@1');
define('SMTP_PORT', 587);
// Set Timezone
define('TIMEZONE', 'Asia/Calcutta');
date_default_timezone_set("Asia/Calcutta");
if ($isLive === TRUE) {
define ('BASE_URL', 'https://trackback.email/'); // the home url of the website
} else {
define ('BASE_URL', 'https://valkya.lan/trackback.email/'); // the home url of the website
}
$currentYear = date('Y');
// Define allowed redirect paths
$allowed_paths = [
'dashboard',
'my-account'
// Add other allowed internal paths here
];
?>
```
### **Issues Identified:**
1. **Hard-Coded SMTP Credentials:**
- **Problem:** Storing SMTP credentials directly in code poses a significant security risk.
2. **Timezone Definition:**
- **Problem:** Correctly setting the timezone, but it's redundant to define a constant if it's not used elsewhere.
3. **`BASE_URL` Definition:**
- **Problem:** The logic is correct but relies on `$isLive` from `config.php`, which is acceptable but can be optimized.
4. **Allowed Paths (`$allowed_paths`):**
- **Problem:** Minimal allowed paths included. Ensure that all necessary paths are covered and managed securely.
### **Recommendations:**
1. **Use Environment Variables for SMTP Credentials:**
- **Reason:** Enhances security by keeping sensitive information out of the codebase.
**Implementation:**
```php
define('SMTP_HOST', getenv('SMTP_HOST') ?: 'smtp.ionos.com');
define('SMTP_USERNAME', getenv('SMTP_USER') ?: 'no-reply@trackback.email');
define('SMTP_PASSWORD', getenv('SMTP_PASS') ?: '');
define('SMTP_PORT', getenv('SMTP_PORT') ?: 587);
```
2. **Manage `BASE_URL` Effectively:**
- **Optional:** If you have more environments (staging, testing, etc.), extend the logic to accommodate them.
**Alternative:**
```php
define('BASE_URL', getenv('BASE_URL') ?: ($isLive ? 'https://trackback.email/' : 'https://valkya.lan/trackback.email/'));
```
3. **Define `TIMEZONE` Globally:**
- **Optional:** If the `TIMEZONE` constant is used across different files, it’s acceptable. Otherwise, rely on `date_default_timezone_set()` alone.
4. **Expand `allowed_paths`:**
- **Problem:** Ensure all legitimate redirect paths are included to prevent unauthorized redirections.
**Suggestion:**
```php
$allowed_paths = [
'dashboard',
'my-account',
'settings',
'profile',
'logout',
// Add all other legitimate paths here
];
```
### **Revised `commonVariables.php`:**
```php
<?php
// Global constants (Using environment variables)
define('SMTP_HOST', getenv('SMTP_HOST') ?: 'smtp.ionos.com');
define('SMTP_USERNAME', getenv('SMTP_USER') ?: 'no-reply@trackback.email');
define('SMTP_PASSWORD', getenv('SMTP_PASS') ?: '');
define('SMTP_PORT', getenv('SMTP_PORT') ?: 587);
// Set Timezone
date_default_timezone_set(getenv('APP_TIMEZONE') ?: 'Asia/Calcutta');
// Define BASE_URL
define('BASE_URL', getenv('BASE_URL') ?: ($isLive ? 'https://trackback.email/' : 'https://valkya.lan/trackback.email/'));
// Current Year
$currentYear = date('Y');
// Define allowed redirect paths
$allowed_paths = [
'dashboard',
'my-account',
'settings',
'profile',
'logout',
// Add other allowed internal paths here
];
?>
```
### **Notes:**
- **Environment Variables:** Ensure that `BASE_URL` and `APP_TIMEZONE` are set appropriately in your environment configurations.
- **Redirection Security:** Regularly audit `allowed_paths` to ensure no unauthorized paths are included, which could be exploited for open redirects.
---
## **3. Review and Improve `commonFunctions.php`**
### **Current Configuration:**
```php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
use DeviceDetector\ClientHints;
use DeviceDetector\DeviceDetector;
use DeviceDetector\Parser\Device\AbstractDeviceParser;
/**
* Generate a random string of a given length using openssl_random_pseudo_bytes
*
* @param int $length The length of the random string to generate (upto maximum 148 characters)
* @return string The random string generated
*/
function generateRandomString($length = 64) {
$hash = openssl_random_pseudo_bytes(16, $crypto_strong);
$randomizeHash = random_bytes(16) . $hash . uniqid() . microtime() . rand();
$randomString = substr(str_shuffle(bin2hex($randomizeHash)), 0, $length);
return $randomString;
}
/**
* Encrypts a cookie value using a secure method.
*
* @param string $value The value to encrypt.
* @return string Encrypted value.
*/
function encryptCookie($value) {
$key = hex2bin(openssl_random_pseudo_bytes(4));
$cipher = "aes-256-cbc";
$ivlen = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext = openssl_encrypt($value, $cipher, $key, 0, $iv);
return(base64_encode($ciphertext . '::' . $iv. '::' .$key));
}
/**
* Decrypts a cookie value using a secure method.
*
* @param string $value The value to decrypt.
* @return string Decrypted value.
*/
function decryptCookie($ciphertext) {
$cipher = "aes-256-cbc";
list($encrypted_data, $iv, $key) = explode('::', base64_decode($ciphertext));
return openssl_decrypt($encrypted_data, $cipher, $key, 0, $iv);
}
/**
* Get real visitor IP behind CloudFlare network
*
* @return string IP address.
*/
function getUserIP() {
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
$_SERVER['HTTP_CLIENT_IP'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
}
$client = @$_SERVER['HTTP_CLIENT_IP'];
$forward = @$_SERVER['HTTP_X_FORWARDED_FOR'];
$remote = $_SERVER['REMOTE_ADDR'];
if (filter_var($client, FILTER_VALIDATE_IP)) {
$ip = $client;
} elseif (filter_var($forward, FILTER_VALIDATE_IP)) {
$ip = $forward;
} else {
$ip = $remote;
}
return $ip;
}
// Function to validate redirect paths
function validate_redirect($path, $allowed_paths) {
// Ensure the path does not contain leading or trailing slashes
$clean_path = trim($path, '/');
// Check if the path is in the allowed list
return in_array($clean_path, $allowed_paths);
}
// Send notification emails from <PRESIDIO_ANONYMIZED_EMAIL_ADDRESS>
function sendEmailNoReply($toEmail, $toName, $subject, $body, $altBody = '', $cc = [], $bcc = [], $attachments = []) {
//Create an instance; passing true enables exceptions
$mail = new PHPMailer(true);
try {
//Server settings
//SMTP::DEBUG_OFF = off (for production use)
//SMTP::DEBUG_CLIENT = client messages
//SMTP::DEBUG_SERVER = client and server messages
$mail->SMTPDebug = SMTP::DEBUG_OFF;
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USERNAME;
$mail->Password = SMTP_PASSWORD;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = SMTP_PORT;
//Extra headers
$mail->Encoding = 'base64';
$mail->XMailer = 'TrackBack.email 1.0.0-alpha (https://trackback.email)';
//Sender
$mail->setFrom('no-reply@trackback.email', 'TrackBack.email');
//Recipients
$mail->addAddress($toEmail, $toName);
//Content
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
if (!empty($altBody)) {
$mail->AltBody = $altBody; // For non-HTML email clients
}
// Add CC recipients
if (!empty($cc)) {
foreach ($cc as $ccEmail) {
$mail->addCC($ccEmail);
}
}
// Add BCC recipients
if (!empty($bcc)) {
foreach ($bcc as $bccEmail) {
$mail->addBCC($bccEmail);
}
}
// Add attachments
if (!empty($attachments)) {
foreach ($attachments as $filePath) {
$mail->addAttachment($filePath);
}
}
$mail->send();
return true;
} catch (Exception $e) {
// Handle exceptions (you can log the error message if needed)
error_log("Mailer Error: " . $mail->ErrorInfo);
return false;
}
}
?>
```
### **Issues Identified:**
1. **Random Key Generation in `encryptCookie`:**
- **Problem:** Each encryption operation generates a new random key and includes it in the encrypted string. This defeats the purpose of encryption as anyone with access to the encrypted value can decrypt it.
2. **Inefficient `generateRandomString`:**
- **Problem:** The function uses multiple sources of randomness (`openssl_random_pseudo_bytes`, `random_bytes`, `uniqid`, `microtime`, `rand`, `str_shuffle`), which is unnecessary and inefficient.
3. **Weak Encryption Practices:**
- **Problem:** Without a consistent encryption key, decryption becomes unreliable and insecure.
4. **Error Suppression in `getUserIP`:**
- **Problem:** Using `@` to suppress errors can hide issues that should be addressed.
5. **Lack of Input Validation in `validate_redirect`:**
- **Problem:** While `validate_redirect` checks against an allowed list, further validation or sanitization could be beneficial.
### **Recommendations:**
1. **Secure Cookie Encryption:**
- **Use a Fixed Server-Side Encryption Key:**
- **Reason:** To ensure that encrypted cookies can be decrypted reliably.
- **Implementation:**
- Store the encryption key securely (e.g., in environment variables).
- Use authenticated encryption (e.g., AES-256-GCM) to ensure data integrity.
**Revised `encryptCookie` and `decryptCookie`:**
```php
/**
* Encrypts a cookie value using a secure method.
*
* @param string $value The value to encrypt.
* @return string Encrypted value.
*/
function encryptCookie($value) {
$key = hex2bin(getenv('COOKIE_ENCRYPTION_KEY')); // Ensure this key is securely stored
$cipher = "aes-256-gcm";
$ivlen = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivlen);
$tag = '';
$ciphertext = openssl_encrypt($value, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag);
return base64_encode($iv . '::' . $tag . '::' . $ciphertext);
}
/**
* Decrypts a cookie value using a secure method.
*
* @param string $ciphertext The encrypted value to decrypt.
* @return string|false Decrypted value or false on failure.
*/
function decryptCookie($ciphertext) {
$key = hex2bin(getenv('COOKIE_ENCRYPTION_KEY'));
$cipher = "aes-256-gcm";
list($iv, $tag, $ciphertext) = explode('::', base64_decode($ciphertext), 3);
return openssl_decrypt($ciphertext, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag);
}
```
- **Generate and Set the Encryption Key Securely:**
- Use a 256-bit key for AES-256-GCM.
- Example:
```bash
# Generate a secure key
openssl rand -hex 32
```
- **Set the Key as an Environment Variable:**
- Add it to your server's environment variables, not in code.
- Example in `.env` file (if using dotenv):
```
COOKIE_ENCRYPTION_KEY=your_generated_key_here
```
2. **Simplify `generateRandomString`:**
- **Use `bin2hex(random_bytes())`:**
- **Reason:** `random_bytes()` provides cryptographically secure randomness, and `bin2hex()` converts it to a readable format.
**Revised `generateRandomString`:**
```php
/**
* Generate a secure random string of a given length.
*
* @param int $length The length of the random string.
* @return string The random string generated.
*/
function generateRandomString($length = 64) {
$bytes = ceil($length / 2);
return substr(bin2hex(random_bytes($bytes)), 0, $length);
}
```
3. **Remove Error Suppression in `getUserIP`:**
- **Reason:** Suppressing errors can hide underlying issues.
**Revised `getUserIP`:**
```php
/**
* Get the real visitor IP behind CloudFlare or other proxies.
*
* @return string IP address.
*/
function getUserIP() {
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
$_SERVER['HTTP_CLIENT_IP'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
}
$client = $_SERVER['HTTP_CLIENT_IP'] ?? null;
$forward = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null;
$remote = $_SERVER['REMOTE_ADDR'] ?? '';
if (filter_var($client, FILTER_VALIDATE_IP)) {
return $client;
} elseif (filter_var($forward, FILTER_VALIDATE_IP)) {
return $forward;
}
return $remote;
}
```
4. **Enhance `validate_redirect`:**
- **Use Strict Comparison:**
```php
function validate_redirect($path, $allowed_paths) {
// Ensure the path does not contain leading or trailing slashes
$clean_path = trim($path, '/');
// Check if the path is in the allowed list using strict comparison
return in_array($clean_path, $allowed_paths, TRUE);
}
```
5. **Improve `sendEmailNoReply`:**
- **Parameter Sanitization:**
- Sanitize email inputs to prevent injection or malformed emails.
- **Handle Attachments Securely:**
- Validate file paths and ensure only intended files are attached.
**Example Enhancements:**
```php
function sendEmailNoReply($toEmail, $toName, $subject, $body, $altBody = '', $cc = [], $bcc = [], $attachments = []) {
// Validate email
if (!filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
error_log("Invalid email address: $toEmail");
return false;
}
//Create an instance; passing true enables exceptions
$mail = new PHPMailer(true);
try {
//Server settings
$mail->SMTPDebug = SMTP::DEBUG_OFF;
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USERNAME;
$mail->Password = SMTP_PASSWORD;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = SMTP_PORT;
//Extra headers
$mail->Encoding = 'base64';
$mail->XMailer = 'TrackBack.email 1.0.0 (https://trackback.email)';
//Sender
$mail->setFrom('no-reply@trackback.email', 'TrackBack.email');
//Recipients
$mail->addAddress($toEmail, $toName);
//Content
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
if (!empty($altBody)) {
$mail->AltBody = $altBody; // For non-HTML email clients
}
// Add CC recipients
foreach ($cc as $ccEmail) {
if (filter_var($ccEmail, FILTER_VALIDATE_EMAIL)) {
$mail->addCC($ccEmail);
} else {
error_log("Invalid CC email address: $ccEmail");
}
}
// Add BCC recipients
foreach ($bcc as $bccEmail) {
if (filter_var($bccEmail, FILTER_VALIDATE_EMAIL)) {
$mail->addBCC($bccEmail);
} else {
error_log("Invalid BCC email address: $bccEmail");
}
}
// Add attachments securely
foreach ($attachments as $filePath) {
if (file_exists($filePath) && is_readable($filePath)) {
$mail->addAttachment($filePath);
} else {
error_log("Attachment not found or unreadable: $filePath");
}
}
$mail->send();
return true;
} catch (Exception $e) {
// Handle exceptions (you can log the error message if needed)
error_log("Mailer Error: " . $mail->ErrorInfo);
return false;
}
}
```
### **Revised `commonFunctions.php`:**
```php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
use DeviceDetector\ClientHints;
use DeviceDetector\DeviceDetector;
use DeviceDetector\Parser\Device\AbstractDeviceParser;
/**
* Generate a secure random string of a specified length.
*
* @param int $length The length of the random string.
* @return string The generated random string.
*/
function generateRandomString($length = 64) {
$bytes = ceil($length / 2);
return substr(bin2hex(random_bytes($bytes)), 0, $length);
}
/**
* Encrypts a cookie value using AES-256-GCM.
*
* @param string $value The value to encrypt.
* @return string|false Encrypted value or false on failure.
*/
function encryptCookie($value) {
$key = hex2bin(getenv('COOKIE_ENCRYPTION_KEY')); // Securely store in environment
$cipher = "aes-256-gcm";
$ivlen = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivlen);
$tag = '';
$ciphertext = openssl_encrypt($value, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag);
if ($ciphertext === FALSE) {
return false;
}
return base64_encode($iv . '::' . $tag . '::' . $ciphertext);
}
/**
* Decrypts a cookie value using AES-256-GCM.
*
* @param string $ciphertext The encrypted value to decrypt.
* @return string|false Decrypted value or false on failure.
*/
function decryptCookie($ciphertext) {
$key = hex2bin(getenv('COOKIE_ENCRYPTION_KEY'));
$cipher = "aes-256-gcm";
$decoded = base64_decode($ciphertext);
if ($decoded === FALSE) {
return false;
}
list($iv, $tag, $ciphertext) = explode('::', $decoded, 3);
return openssl_decrypt($ciphertext, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag);
}
/**
* Get the real visitor IP behind CloudFlare or other proxies.
*
* @return string IP address.
*/
function getUserIP() {
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
$_SERVER['HTTP_CLIENT_IP'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
}
$client = $_SERVER['HTTP_CLIENT_IP'] ?? null;
$forward = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null;
$remote = $_SERVER['REMOTE_ADDR'] ?? '';
if (filter_var($client, FILTER_VALIDATE_IP)) {
return $client;
} elseif (filter_var($forward, FILTER_VALIDATE_IP)) {
// In case of multiple IPs, take the first valid one
$ipList = explode(',', $forward);
foreach ($ipList as $ip) {
$ip = trim($ip);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return $remote;
}
/**
* Validate if the redirect path is allowed.
*
* @param string $path The path to validate.
* @param array $allowed_paths The list of allowed paths.
* @return bool True if allowed, false otherwise.
*/
function validate_redirect($path, $allowed_paths) {
// Ensure the path does not contain leading or trailing slashes
$clean_path = trim($path, '/');
// Check if the path is in the allowed list using strict comparison
return in_array($clean_path, $allowed_paths, true);
}
/**
* Send notification emails from <PRESIDIO_ANONYMIZED_EMAIL_ADDRESS>.
*
* @param string $toEmail Recipient's email address.
* @param string $toName Recipient's name.
* @param string $subject Email subject.
* @param string $body HTML body of the email.
* @param string $altBody Plain text alternative body.
* @param array $cc Array of CC email addresses.
* @param array $bcc Array of BCC email addresses.
* @param array $attachments Array of file paths to attach.
* @return bool True if sent successfully, false otherwise.
*/
function sendEmailNoReply($toEmail, $toName, $subject, $body, $altBody = '', $cc = [], $bcc = [], $attachments = []) {
// Validate email address
if (!filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
error_log("Invalid recipient email address: $toEmail");
return false;
}
//Create an instance; passing true enables exceptions
$mail = new PHPMailer(true);
try {
//Server settings
$mail->SMTPDebug = SMTP::DEBUG_OFF; // Disable verbose debug output
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USERNAME;
$mail->Password = SMTP_PASSWORD;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = SMTP_PORT;
//Extra headers
$mail->Encoding = 'base64';
$mail->XMailer = 'TrackBack.email 1.0.0 (https://trackback.email)';
//Sender
$mail->setFrom('no-reply@trackback.email', 'TrackBack.email');
//Recipients
$mail->addAddress($toEmail, $toName);
//Content
$mail->isHTML(true);
$mail->Subject = htmlspecialchars($subject, ENT_QUOTES, 'UTF-8');
$mail->Body = $body;
if (!empty($altBody)) {
$mail->AltBody = $altBody; // For non-HTML email clients
}
// Add CC recipients
foreach ($cc as $ccEmail) {
if (filter_var($ccEmail, FILTER_VALIDATE_EMAIL)) {
$mail->addCC($ccEmail);
} else {
error_log("Invalid CC email address: $ccEmail");
}
}
// Add BCC recipients
foreach ($bcc as $bccEmail) {
if (filter_var($bccEmail, FILTER_VALIDATE_EMAIL)) {
$mail->addBCC($bccEmail);
} else {
error_log("Invalid BCC email address: $bccEmail");
}
}
// Add attachments securely
foreach ($attachments as $filePath) {
if (file_exists($filePath) && is_readable($filePath)) {
$mail->addAttachment($filePath);
} else {
error_log("Attachment not found or unreadable: $filePath");
}
}
$mail->send();
return true;
} catch (Exception $e) {
// Log the detailed error for debugging
error_log("Mailer Error: " . $mail->ErrorInfo);
return false;
}
}
?>
```
### **Key Enhancements:**
1. **Secure Encryption:**
- **Fixed Encryption Key:** Replace dynamic key generation with a fixed server-side key stored securely via environment variables.
- **Authenticated Encryption:** Use AES-256-GCM for authenticated encryption, ensuring data integrity.
2. **Simplified Random String Generation:**
- **Use `random_bytes` and `bin2hex`:** Provides sufficient randomness securely and efficiently.
3. **Improved `getUserIP`:**
- **Removed Error Suppression:** Avoid using `@` to ensure that any unexpected issues are logged and addressed.
- **Multiple IP Handling:** Properly handles scenarios with multiple forwarded IPs.
4. **Strict Path Validation:**
- **Use Strict Comparison in `validate_redirect`:** Prevents false positives by ensuring type-safe comparisons.
5. **Enhanced Email Sending:**
- **Email Validation:** Validate all email addresses before attempting to send.
- **Securely Handle Attachments:** Ensure that files exist and are readable before attaching.
- **Sanitize Email Content:** Prevent potential XSS by sanitizing subject and optionally the body.
- **Error Logging:** Log errors instead of exposing them to users.
6. **Use of Environment Variables:**
- **SMTP Credentials and Encryption Keys:** Retrieve these from environment variables to avoid hard-coding sensitive information.
### **Notes:**
- **Environment Variables:** Use tools like [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv) to manage environment variables systematically.
- **Logging:** Ensure that all critical errors are logged for monitoring and debugging purposes.
---
## **4. Step-by-Step Guide to Enhance Your Login Code**
Given the comprehensive review of your included files, let's proceed to systematically improve your main login script based on best practices, security enhancements, and maintainability improvements.
### **Current Main Login Script:**
```php
<?php
// All includes and setup
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
use DeviceDetector\ClientHints;
use DeviceDetector\DeviceDetector;
use DeviceDetector\Parser\Device\AbstractDeviceParser;
include 'includes/config.php';
include 'includes/commonVariables.php';
include 'includes/commonFunctions.php';
require 'vendor/autoload.php';
// Initialize the session
session_start();
// If the user is already logged in, redirect them appropriately
if (isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true) {
// Determine where to redirect the user
if (isset($_SESSION['redirect']) && !empty($_SESSION['redirect']) && validate_redirect($_SESSION['redirect'], $allowed_paths)) {
$location = $_SESSION['redirect'];
// Clear the redirect session variable to prevent reuse
unset($_SESSION['redirect']);
} else {
$location = 'dashboard';
}
// Redirect using BASE_URL and $location
header('Location: ' . BASE_URL . $location); // No double slashes
exit;
}
// Initialize variables
$location = 'dashboard'; // Default location
// Determine the redirection location after login
if (isset($_SESSION['redirect']) && !empty($_SESSION['redirect']) && validate_redirect($_SESSION['redirect'], $allowed_paths)) {
$location = $_SESSION['redirect'];
}
// CSRF token generation and session check
if (!isset($_SESSION["csrf_token"]) || empty($_SESSION['csrf_token'])) {
$csrf_token = generateRandomString(32);
$_SESSION["csrf_token"] = $csrf_token;
} else {
$csrf_token = $_SESSION['csrf_token'];
}
// Check if the user is already logged in, if yes then redirect to dashboard page
if (isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true) {
header('location:' . BASE_URL . $location);
exit;
} elseif (isset($_COOKIE['rememberme'])) {
// Decrypt cookie value
$token = decryptCookie($_COOKIE['rememberme']);
$sql_query = "SELECT `uid`, `email`, `token` FROM `users` WHERE `token` = ?";
if ($stmt = mysqli_prepare($conn, $sql_query)) {
mysqli_stmt_bind_param($stmt, "s", $token);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_bind_result($stmt, $uid, $email, $db_token);
mysqli_stmt_fetch($stmt);
if ($db_token === $token) {
$_SESSION["uid"] = $uid;
$_SESSION["email"] = $email;
$_SESSION["token"] = $token;
$_SESSION["loggedin"] = true; // Set the logged-in flag
header('Location: ' . BASE_URL . $location);
exit;
}
}
mysqli_stmt_close($stmt);
}
}
// Define variables and initialize with empty values
$email = $password = "";
$email_err = $password_err = $errorMSG = "";
// Processing form data when form is submitted
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Validate CSRF token
if (!isset($_POST['csrf_token']) || empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
$errorMSG = "Invalid CSRF token. Please try again.";
} else {
// Check if email is empty
if (empty(trim($_POST["email"]))) {
$email_err = "Please enter your email.";
} else {
$email = trim($_POST["email"]);
}
// Check if password is empty
if (empty(trim($_POST["password"]))) {
$password_err = "Please enter your password.";
} else {
$password = trim($_POST["password"]);
}
// Validate credentials
if (empty($email_err) && empty($password_err)) {
$sql = "SELECT `uid`, `email`, `password`, `active`, `token` FROM `users` WHERE `email` = ?";
if ($stmt = mysqli_prepare($conn, $sql)) {
mysqli_stmt_bind_param($stmt, "s", $email);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_store_result($stmt);
if (mysqli_stmt_num_rows($stmt) === 1) {
mysqli_stmt_bind_result($stmt, $uid, $db_email, $hashed_password, $isActive, $token);
if (mysqli_stmt_fetch($stmt)) {
if (password_verify($password, $hashed_password)) {
if ($isActive === 1) {
// Start session and set session variables
session_regenerate_id(); // Prevent session fixation attacks
$_SESSION["loggedin"] = true;
$_SESSION["uid"] = $uid;
$_SESSION["email"] = $db_email;
$_SESSION["token"] = $token;
// Fetch the previous last_login and last_login_ip details
$sql_fetch = "SELECT `first_name`, `last_name`, `last_login`, `last_login_ip` FROM `users` WHERE `uid` = ?";
if ($stmt_fetch = mysqli_prepare($conn, $sql_fetch)) {
mysqli_stmt_bind_param($stmt_fetch, "i", $uid); // Assuming $uid is already available
mysqli_stmt_execute($stmt_fetch);
mysqli_stmt_bind_result($stmt_fetch, $first_name, $last_name, $prev_last_login, $prev_last_login_ip);
mysqli_stmt_fetch($stmt_fetch);
mysqli_stmt_close($stmt_fetch);
// Store the previous login details in session variables
$_SESSION["prev_last_login"] = $prev_last_login;
$_SESSION["prev_last_login_ip"] = $prev_last_login_ip;
$fullName = $first_name . " " . $last_name;
}
// Check if "remember me" was selected
if (isset($_POST["rememberme"])) {
$_SESSION["rememberme"] = true;
// Generate and store token as before
$encryptedToken = encryptCookie($_SESSION["token"]);
// Extract the domain from BASE_URL
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
// Set the rememberme cookie using array syntax
setcookie('rememberme', $encryptedToken, [
'expires' => time() + (86400 * 30), // 30 days
'path' => '/',
'domain' => $domain, // Correctly set the domain
'secure' => true, // Only send cookie over HTTPS
'httponly' => true, // Accessible only through the HTTP protocol
'samesite' => 'Strict' // Adjust as needed (None, Lax, Strict)
]);
} else {
$_SESSION["rememberme"] = false;
// Optionally, delete the rememberme cookie
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
setcookie('rememberme', '', [
'expires' => time() - 3600, // Set time in the past to delete
'path' => '/',
'domain' => $domain, // Correctly set the domain
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
// Update `last_login` and `last_login_ip` in `users` table
$last_login = date('Y-m-d H:i:s');
$last_login_ip = getUserIP();
$sql_update = "UPDATE `users` SET `last_login` = ?, `last_login_ip` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update)) {
mysqli_stmt_bind_param($stmt_update, "sss", $last_login, $last_login_ip, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
$queryNotification = "SELECT notification_pref_security FROM user_profile WHERE uid = ?";
$stmtNotification = mysqli_prepare($conn, $queryNotification);
mysqli_stmt_bind_param($stmtNotification, 'i', $_SESSION["uid"]);
mysqli_stmt_execute($stmtNotification);
mysqli_stmt_store_result($stmtNotification);
if (mysqli_stmt_num_rows($stmtNotification) > 0) {
mysqli_stmt_bind_result($stmtNotification, $notif_security);
mysqli_stmt_fetch($stmtNotification);
} else {
$notif_security = 1;
}
mysqli_stmt_close($stmtNotification);
if ($notif_security === 1 && $_SESSION["prev_last_login_ip"] != $last_login_ip) {
$last_login_datetime = date('M d, Y, h:i A', strtotime($last_login));
AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_NONE);
$userAgent = $_SERVER['HTTP_USER_AGENT']; // change this to the useragent you want to parse
$clientHints = ClientHints::factory($_SERVER); // client hints are optional
$dd = new DeviceDetector($userAgent, $clientHints);
$dd->parse();
if ($dd->isBot()) {
// handle bots,spiders,crawlers,...
$botInfo = $dd->getBot();
} else {
$clientInfo = $dd->getClient(); // holds information about browser, feed reader, media player, ...
$osInfo = $dd->getOs();
$device = $dd->getDeviceName();
$brand = $dd->getBrandName();
$model = $dd->getModel();
}
$ipBase = new \Ipbase\Ipbase\IpbaseClient('ipb_live_BSBfuKApOLL6vkZ1SJCzP8oWbygM1ncMUzBd6iVd');
$ipBaseData = $ipBase->info();
//$ipBaseData = json_decode($ipBaseDataJSON, true);
$ipBaseDataLocation = $ipBaseData['data']['location'];
$city = $ipBaseDataLocation['city']['name'];
$region = $ipBaseDataLocation['region']['name'];
$country = $ipBaseDataLocation['country']['name'];
$location_string = "$city, $region, $country";
$emailBody = file_get_contents('templates/newLoginEmailTemplate.html');
$toReplace = ['::BASEURL::', '::FULLNAME::', '::CURRENTYEAR::', '::DEVICE::', '::CLIENT::', '::OS::', '::LOCATION::', '::DATETIME::'];
$withThis = [BASE_URL, $fullName, $currentYear, ucfirst($device), $clientInfo['name'], $osInfo['name'], $location_string, $last_login_datetime];
$emailBody = str_replace($toReplace, $withThis, $emailBody);
// Prepare email variables
$toEmail = $email; // Recipient's email address
$toName = $fullName; // Recipient's full name
$subject = 'New Device Login Alert for Your TrackBack.email Account';
$body = $emailBody; // Your HTML email body
// Send the email
if (sendEmailNoReply($toEmail, $toName, $subject, $body)) {
$msg = "Successfully logged in!";
} else {
$errorMSG = "New Login detected email could not be sent.<br>Kindly contact us for further assistance.";
}
}
unset($_SESSION['csrf_token']); // Unset session token after submitting
header('location:' . BASE_URL . $location);
} else {
// Account is not activated
$errorMSG = "Your account is not activated yet.<br>Please check your inbox for activation email.";
}
} else {
// Password is incorrect
$errorMSG = "Invalid email or password.";
}
}
} else {
// Email doesn't exist
$errorMSG = "Invalid email or password.";
}
} else {
$errorMSG = "Oops! Something went wrong. Please try again later.";
}
mysqli_stmt_close($stmt);
}
}
}
mysqli_close($conn);
}
include 'includes/start.php';
?>
```
### **Issues Identified:**
1. **Redundant Session Checks:**
- **Problem:** The script checks if the user is logged in twice (`isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true`). This redundancy complicates the logic.
2. **Insecure 'Remember Me' Implementation:**
- **Problem:** The `decryptCookie` function uses dynamic keys per encryption, making token reuse vulnerable.
3. **CSRF Token Handling:**
- **Problem:** CSRF tokens are not regenerated after form submission, potentially allowing replay attacks.
4. **Password Verification and Session Management:**
- **Problem:** Missing robust error handling during authentication and session fixation prevention measures.
5. **Incomplete 'Remember Me' Security:**
- **Problem:** Tokens are not rotated or invalidated upon usage, increasing the risk of replay attacks.
6. **Error Handling and Logging:**
- **Problem:** Errors during database operations are not logged, making debugging difficult.
7. **Email Sending Security:**
- **Problem:** Emails are sent without validation or ensuring attachment security, increasing risks of injection or file manipulation.
8. **Potential XSS Vulnerabilities:**
- **Problem:** User inputs like `$fullName` are directly used in emails without proper sanitization.
9. **External API Key Exposure:**
- **Problem:** API keys (e.g., `IpbaseClient` key) are hard-coded, which is a security risk.
### **Step-by-Step Enhancements:**
#### **Step 1: Consolidate Session Checks**
**Issue:**
Multiple checks for `$_SESSION["loggedin"]` cause redundancy.
**Solution:**
Create helper functions to manage session checks and redirections.
**Implementation:**
1. **Define Helper Functions in `commonFunctions.php`:**
```php
/**
* Check if the user is logged in.
*
* @return bool True if logged in, false otherwise.
*/
function isUserLoggedIn() {
return isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true;
}
/**
* Get the redirect location.
*
* @global array $allowed_paths
* @return string The redirect path.
*/
function getRedirectLocation() {
global $allowed_paths;
if (isset($_SESSION['redirect']) && !empty($_SESSION['redirect']) && validate_redirect($_SESSION['redirect'], $allowed_paths)) {
$location = $_SESSION['redirect'];
unset($_SESSION['redirect']);
} else {
$location = 'dashboard';
}
return $location;
}
/**
* Redirect the user to a specified location.
*
* @param string $location The location to redirect to.
*/
function redirectTo($location) {
// Prevent header injection
$location = str_replace(array("\r", "\n"), '', $location);
header('Location: ' . BASE_URL . $location);
exit;
}
```
2. **Use Helper Functions in Main Script:**
```php
// If the user is already logged in, redirect them appropriately
if (isUserLoggedIn()) {
$location = getRedirectLocation();
redirectTo($location);
}
```
3. **Remove Redundant Checks:**
- Delete the second login check to avoid duplication.
#### **Step 2: Secure 'Remember Me' Implementation**
**Issue:**
Currently, the 'remember me' token uses dynamic encryption keys and lacks proper token management.
**Solution:**
Implement a secure token-based 'remember me' system with hashed tokens, token rotation, and proper session management.
**Implementation:**
1. **Modify Token Storage:**
- **Store Hashed Tokens:** Store a hashed version of the token in the database to prevent token exposure.
- **Associate Tokens with Users and Devices:** Optionally, add fields like `device_name`, `expires_at`, etc., for enhanced security.
**Database Schema Adjustment:**
```sql
ALTER TABLE `users` ADD COLUMN `remember_token_hash` VARCHAR(64) NULL AFTER `token`;
ALTER TABLE `users` ADD COLUMN `remember_token_expires` DATETIME NULL AFTER `remember_token_hash`;
```
2. **Generate and Store 'Remember Me' Token:**
- **Generate Token:** Use secure random bytes.
- **Hash Token:** Use a secure hashing algorithm.
- **Store Hash and Expiration in Database.**
**Implementation:**
```php
if (isset($_POST["rememberme"])) {
// Generate secure token
$token = generateRandomString(64);
$hashed_token = hash('sha256', $token);
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
// Update token in database
$sql_update_token = "UPDATE `users` SET `remember_token_hash` = ?, `remember_token_expires` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update_token)) {
mysqli_stmt_bind_param($stmt_update, "ssi", $hashed_token, $expires_at, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
// Encrypt token before setting cookie
$encryptedToken = encryptCookie($token);
// Set the 'rememberme' cookie
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
setcookie('rememberme', $encryptedToken, [
'expires' => time() + (86400 * 30), // 30 days
'path' => '/',
'domain' => $domain,
'secure' => true, // Only send cookie over HTTPS
'httponly' => true, // Accessible only through the HTTP protocol
'samesite' => 'Strict' // Adjust as needed (None, Lax, Strict)
]);
} else {
// Delete the 'rememberme' cookie and remove token from database
if (isset($_COOKIE['rememberme'])) {
// Get user ID from session or other means
$uid = $_SESSION["uid"] ?? null;
if ($uid) {
$sql_clear_token = "UPDATE `users` SET `remember_token_hash` = NULL, `remember_token_expires` = NULL WHERE `uid` = ?";
if ($stmt_clear = mysqli_prepare($conn, $sql_clear_token)) {
mysqli_stmt_bind_param($stmt_clear, "i", $uid);
mysqli_stmt_execute($stmt_clear);
mysqli_stmt_close($stmt_clear);
}
}
// Delete the cookie
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
setcookie('rememberme', '', [
'expires' => time() - 3600, // Past time to delete
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
}
```
3. **Handle 'Remember Me' Auto-Login Securely:**
**Implementation:**
```php
// Handle 'remember me' auto-login
if (isset($_COOKIE['rememberme'])) {
$token = decryptCookie($_COOKIE['rememberme']);
if ($token) {
$hashed_token = hash('sha256', $token);
$sql_query = "SELECT `uid`, `email`, `remember_token_hash`, `remember_token_expires` FROM `users` WHERE `remember_token_hash` = ?";
if ($stmt = mysqli_prepare($conn, $sql_query)) {
mysqli_stmt_bind_param($stmt, "s", $hashed_token);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_store_result($stmt);
if (mysqli_stmt_num_rows($stmt) === 1) {
mysqli_stmt_bind_result($stmt, $uid, $email, $db_hashed_token, $token_expires);
if (mysqli_stmt_fetch($stmt)) {
if (hash_equals($db_hashed_token, $hashed_token) && strtotime($token_expires) > time()) {
// Valid token
session_regenerate_id(true);
$_SESSION["uid"] = $uid;
$_SESSION["email"] = $email;
$_SESSION["loggedin"] = true;
// Update `last_login` and `last_login_ip`
$last_login = date('Y-m-d H:i:s');
$last_login_ip = getUserIP();
$sql_update = "UPDATE `users` SET `last_login` = ?, `last_login_ip` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update)) {
mysqli_stmt_bind_param($stmt_update, "ssi", $last_login, $last_login_ip, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
// Optionally, rotate the token
// Generate a new token, store its hash, set new cookie
redirectTo(getRedirectLocation());
}
}
}
}
mysqli_stmt_close($stmt);
}
}
// Invalid token: delete the cookie
setcookie('rememberme', '', [
'expires' => time() - 3600,
'path' => '/',
'domain' => parse_url(BASE_URL, PHP_URL_HOST),
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
```
4. **Implement CSRF Token Rotation:**
**Issue:**
CSRF tokens are not regenerated after form submissions, allowing potential replay attacks.
**Solution:**
Regenerate CSRF tokens after successful form processing.
**Implementation:**
```php
// After successful login
unset($_SESSION['csrf_token']);
$_SESSION['csrf_token'] = generateRandomString(32); // Generate a new token
```
5. **Enhance Password Handling and Verification:**
**Issue:**
Missing checks for password rehashing and ensuring password strength during registration (not shown here but important).
**Solution:**
- **Rehash Passwords if Needed:**
Use `password_needs_rehash()` to upgrade password hashes when the hashing algorithm changes.
**Implementation:**
```php
if (password_verify($password, $hashed_password)) {
if (password_needs_rehash($hashed_password, PASSWORD_DEFAULT)) {
$new_hashed_password = password_hash($password, PASSWORD_DEFAULT);
// Update the password in the database
$sql_rehash = "UPDATE `users` SET `password` = ? WHERE `uid` = ?";
if ($stmt_rehash = mysqli_prepare($conn, $sql_rehash)) {
mysqli_stmt_bind_param($stmt_rehash, "si", $new_hashed_password, $uid);
mysqli_stmt_execute($stmt_rehash);
mysqli_stmt_close($stmt_rehash);
}
}
// Proceed with login...
}
```
6. **Implement Robust Error Handling and Logging:**
**Issue:**
Errors during authentication are set as messages but not logged, which complicates debugging.
**Solution:**
Log all critical errors while providing user-friendly messages.
**Implementation:**
```php
// After failed MySQL operations
if (!$stmt) {
error_log("MySQL Prepare Error: " . mysqli_error($conn));
$errorMSG = "An unexpected error occurred. Please try again later.";
}
```
7. **Sanitize Outputs to Prevent XSS:**
**Issue:**
User inputs and dynamic content like `$fullName` are used directly without sanitization, risking XSS attacks.
**Solution:**
Use `htmlspecialchars()` when outputting user data.
**Implementation:**
```php
// When displaying error messages
echo htmlspecialchars($errorMSG, ENT_QUOTES, 'UTF-8');
// When embedding user data in email templates
$withThis = [
BASE_URL,
htmlspecialchars($fullName, ENT_QUOTES, 'UTF-8'),
htmlspecialchars($currentYear, ENT_QUOTES, 'UTF-8'),
htmlspecialchars(ucfirst($device), ENT_QUOTES, 'UTF-8'),
htmlspecialchars($clientInfo['name'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars($osInfo['name'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars($location_string, ENT_QUOTES, 'UTF-8'),
htmlspecialchars($last_login_datetime, ENT_QUOTES, 'UTF-8')
];
```
8. **Secure API Keys and External Services:**
**Issue:**
API keys (e.g., `IpbaseClient` key) are hard-coded, leading to security vulnerabilities.
**Solution:**
Store API keys securely using environment variables.
**Implementation:**
```php
$ipBase = new \Ipbase\Ipbase\IpbaseClient(getenv('IPBASE_API_KEY'));
```
**Ensure:**
- API keys are set as environment variables and not exposed in the codebase.
- Restrict API key permissions to the minimum required.
9. **Optimize and Structure Database Queries:**
**Issue:**
Multiple queries fetching user information can lead to inefficiency.
**Solution:**
Consolidate queries to retrieve all necessary user data in a single query.
**Implementation:**
```php
// Consolidated query
$sql = "SELECT `uid`, `email`, `password`, `active`, `token`, `first_name`, `last_name`, `last_login`, `last_login_ip` FROM `users` WHERE `email` = ?";
if ($stmt = mysqli_prepare($conn, $sql)) {
mysqli_stmt_bind_param($stmt, "s", $email);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_store_result($stmt);
if (mysqli_stmt_num_rows($stmt) === 1) {
mysqli_stmt_bind_result($stmt, $uid, $db_email, $hashed_password, $isActive, $token, $first_name, $last_name, $prev_last_login, $prev_last_login_ip);
if (mysqli_stmt_fetch($stmt)) {
// Proceed with authentication
}
}
}
}
```
10. **Enhance Security in Device and IP Detection:**
**Issue:**
Device and IP detection logic is intertwined with authentication, potentially leading to performance issues or security flaws.
**Solution:**
Modularize device and IP detection, handle failures gracefully, and ensure that API interactions are secure.
**Implementation:**
```php
if ($notif_security === 1 && $_SESSION["prev_last_login_ip"] != $last_login_ip) {
$last_login_datetime = date('M d, Y, h:i A', strtotime($last_login));
AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_NONE);
$userAgent = $_SERVER['HTTP_USER_AGENT'];
$clientHints = ClientHints::factory($_SERVER);
$dd = new DeviceDetector($userAgent, $clientHints);
$dd->parse();
if ($dd->isBot()) {
$botInfo = $dd->getBot();
// Optionally, skip sending email for bots
continue;
} else {
$clientInfo = $dd->getClient(); // Browser info
$osInfo = $dd->getOs();
$device = $dd->getDeviceName();
}
// IP Geolocation
$ipBase = new \Ipbase\Ipbase\IpbaseClient(getenv('IPBASE_API_KEY'));
$ipBaseData = $ipBase->info(getUserIP()); // Pass the user IP explicitly
if (isset($ipBaseData['data']['location'])) {
$ipBaseDataLocation = $ipBaseData['data']['location'];
$city = $ipBaseDataLocation['city']['name'];
$region = $ipBaseDataLocation['region']['name'];
$country = $ipBaseDataLocation['country']['name'];
$location_string = "$city, $region, $country";
} else {
$location_string = 'Unknown Location';
}
// Prepare and send email as before, ensuring all data is sanitized
}
```
11. **Implement Rate Limiting and Brute-Force Protection (Already Addressed in Previous Recommendations):**
**Implement Features Like:**
- Tracking failed login attempts.
- Temporarily locking accounts after multiple failures.
- Introducing CAPTCHAs after several failed attempts.
### **Revised Main Login Script:**
Considering the above recommendations, here's an enhanced version of your main login script.
```php
<?php
// Autoload dependencies
require_once 'vendor/autoload.php';
// Include required files
require_once 'includes/config.php';
require_once 'includes/commonVariables.php';
require_once 'includes/commonFunctions.php';
// Initialize the session (session_set_cookie_params already handled in config.php)
// session_start(); // Already called in config.php
// Handle 'Remember Me' Auto-Login
if (isset($_COOKIE['rememberme'])) {
$token = decryptCookie($_COOKIE['rememberme']);
if ($token) {
$hashed_token = hash('sha256', $token);
$sql_query = "SELECT `uid`, `email`, `remember_token_hash`, `remember_token_expires` FROM `users` WHERE `remember_token_hash` = ?";
if ($stmt = mysqli_prepare($conn, $sql_query)) {
mysqli_stmt_bind_param($stmt, "s", $hashed_token);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_store_result($stmt);
if (mysqli_stmt_num_rows($stmt) === 1) {
mysqli_stmt_bind_result($stmt, $uid, $email, $db_hashed_token, $token_expires);
if (mysqli_stmt_fetch($stmt)) {
if (hash_equals($db_hashed_token, $hashed_token) && strtotime($token_expires) > time()) {
// Valid token
session_regenerate_id(true);
$_SESSION["uid"] = $uid;
$_SESSION["email"] = $email;
$_SESSION["loggedin"] = true;
// Update `last_login` and `last_login_ip`
$last_login = date('Y-m-d H:i:s');
$last_login_ip = getUserIP();
$sql_update = "UPDATE `users` SET `last_login` = ?, `last_login_ip` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update)) {
mysqli_stmt_bind_param($stmt_update, "ssi", $last_login, $last_login_ip, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
// Rotate 'remember me' token for enhanced security
$newToken = generateRandomString(64);
$newHashedToken = hash('sha256', $newToken);
$newExpiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
$sql_rotate_token = "UPDATE `users` SET `remember_token_hash` = ?, `remember_token_expires` = ? WHERE `uid` = ?";
if ($stmt_rotate = mysqli_prepare($conn, $sql_rotate_token)) {
mysqli_stmt_bind_param($stmt_rotate, "ssi", $newHashedToken, $newExpiresAt, $uid);
mysqli_stmt_execute($stmt_rotate);
mysqli_stmt_close($stmt_rotate);
}
// Set new 'rememberme' cookie
$encryptedNewToken = encryptCookie($newToken);
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
setcookie('rememberme', $encryptedNewToken, [
'expires' => time() + (86400 * 30), // 30 days
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
redirectTo(getRedirectLocation());
}
}
}
}
mysqli_stmt_close($stmt);
}
}
// Invalid token: delete the cookie
setcookie('rememberme', '', [
'expires' => time() - 3600,
'path' => '/',
'domain' => parse_url(BASE_URL, PHP_URL_HOST),
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
// If the user is already logged in, redirect them appropriately
if (isUserLoggedIn()) {
redirectTo(getRedirectLocation());
}
// Initialize variables
$email = $password = "";
$email_err = $password_err = $errorMSG = "";
// Generate CSRF Token (already handled earlier)
// $csrf_token = isset($_SESSION['csrf_token']) ? $_SESSION['csrf_token'] : generateRandomString(32);
// Processing form data when form is submitted
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// Validate CSRF token
if (!isset($_POST['csrf_token']) || empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
$errorMSG = "Invalid CSRF token. Please try again.";
logError("CSRF token mismatch for session ID: " . session_id());
} else {
// Sanitize and validate email
$email = filter_var(trim($_POST["email"]), FILTER_SANITIZE_EMAIL);
if (empty($email)) {
$email_err = "Please enter your email.";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$email_err = "Please enter a valid email address.";
}
// Validate password
$password = trim($_POST["password"]);
if (empty($password)) {
$password_err = "Please enter your password.";
} elseif (strlen($password) < 8) {
$password_err = "Password must be at least 8 characters long.";
}
// Proceed if no validation errors
if (empty($email_err) && empty($password_err)) {
$sql = "SELECT `uid`, `email`, `password`, `active`, `token`, `first_name`, `last_name`, `last_login`, `last_login_ip` FROM `users` WHERE `email` = ?";
if ($stmt = mysqli_prepare($conn, $sql)) {
mysqli_stmt_bind_param($stmt, "s", $email);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_store_result($stmt);
if (mysqli_stmt_num_rows($stmt) === 1) {
mysqli_stmt_bind_result($stmt, $uid, $db_email, $hashed_password, $isActive, $token, $first_name, $last_name, $prev_last_login, $prev_last_login_ip);
if (mysqli_stmt_fetch($stmt)) {
if (password_verify($password, $hashed_password)) {
if ($isActive === 1) {
// Valid credentials and account active
session_regenerate_id(true);
$_SESSION["loggedin"] = true;
$_SESSION["uid"] = $uid;
$_SESSION["email"] = $db_email;
$_SESSION["token"] = $token;
$_SESSION["prev_last_login"] = $prev_last_login;
$_SESSION["prev_last_login_ip"] = $prev_last_login_ip;
$fullName = htmlspecialchars($first_name . " " . $last_name, ENT_QUOTES, 'UTF-8');
// Handle 'remember me' functionality (as previously implemented)
if (isset($_POST["rememberme"])) {
// Generate secure token
$token = generateRandomString(64);
$hashed_token = hash('sha256', $token);
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
// Update token in database
$sql_update_token = "UPDATE `users` SET `remember_token_hash` = ?, `remember_token_expires` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update_token)) {
mysqli_stmt_bind_param($stmt_update, "ssi", $hashed_token, $expires_at, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
// Encrypt token before setting cookie
$encryptedToken = encryptCookie($token);
// Extract domain from BASE_URL
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
// Set the 'rememberme' cookie
setcookie('rememberme', $encryptedToken, [
'expires' => time() + (86400 * 30), // 30 days
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
} else {
// Delete the 'rememberme' cookie and remove token from database
if (isset($_COOKIE['rememberme'])) {
$sql_clear_token = "UPDATE `users` SET `remember_token_hash` = NULL, `remember_token_expires` = NULL WHERE `uid` = ?";
if ($stmt_clear = mysqli_prepare($conn, $sql_clear_token)) {
mysqli_stmt_bind_param($stmt_clear, "i", $uid);
mysqli_stmt_execute($stmt_clear);
mysqli_stmt_close($stmt_clear);
}
// Delete the cookie
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
setcookie('rememberme', '', [
'expires' => time() - 3600, // Past time to delete
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
}
// Update `last_login` and `last_login_ip` in `users` table
$last_login = date('Y-m-d H:i:s');
$last_login_ip = getUserIP();
$sql_update = "UPDATE `users` SET `last_login` = ?, `last_login_ip` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update)) {
mysqli_stmt_bind_param($stmt_update, "ssi", $last_login, $last_login_ip, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
// Fetch notification preferences
$queryNotification = "SELECT `notification_pref_security` FROM `user_profile` WHERE `uid` = ?";
if ($stmtNotification = mysqli_prepare($conn, $queryNotification)) {
mysqli_stmt_bind_param($stmtNotification, 'i', $uid);
mysqli_stmt_execute($stmtNotification);
mysqli_stmt_bind_result($stmtNotification, $notif_security);
mysqli_stmt_store_result($stmtNotification);
if (mysqli_stmt_num_rows($stmtNotification) > 0) {
mysqli_stmt_fetch($stmtNotification);
} else {
$notif_security = 1; // Default to enabled
}
mysqli_stmt_close($stmtNotification);
}
// Check for new IP login and send notification
if ($notif_security === 1 && $_SESSION["prev_last_login_ip"] !== $last_login_ip) {
// Device and IP information parsing
AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_NONE);
$userAgent = $_SERVER['HTTP_USER_AGENT'];
$clientHints = ClientHints::factory($_SERVER);
$dd = new DeviceDetector($userAgent, $clientHints);
$dd->parse();
if ($dd->isBot()) {
// Optionally skip sending email for bots
// $botInfo = $dd->getBot();
// You might decide to log this instead
} else {
$clientInfo = $dd->getClient();
$osInfo = $dd->getOs();
$device = $dd->getDeviceName();
}
// IP Geolocation
$ipBase = new \Ipbase\Ipbase\IpbaseClient(getenv('IPBASE_API_KEY'));
$ipDetails = $ipBase->info($last_login_ip); // Pass user IP explicitly
if (isset($ipDetails['data']['location'])) {
$ipBaseDataLocation = $ipDetails['data']['location'];
$city = $ipBaseDataLocation['city']['name'] ?? 'Unknown City';
$region = $ipBaseDataLocation['region']['name'] ?? 'Unknown Region';
$country = $ipBaseDataLocation['country']['name'] ?? 'Unknown Country';
$location_string = "$city, $region, $country";
} else {
$location_string = 'Unknown Location';
}
// Prepare email
$last_login_datetime = date('M d, Y, h:i A', strtotime($last_login));
$emailBody = file_get_contents('templates/newLoginEmailTemplate.html');
$toReplace = [
'::BASEURL::',
'::FULLNAME::',
'::CURRENTYEAR::',
'::DEVICE::',
'::CLIENT::',
'::OS::',
'::LOCATION::',
'::DATETIME::'
];
$withThis = [
htmlspecialchars(BASE_URL, ENT_QUOTES, 'UTF-8'),
$fullName,
htmlspecialchars($currentYear, ENT_QUOTES, 'UTF-8'),
htmlspecialchars(ucfirst($device), ENT_QUOTES, 'UTF-8'),
htmlspecialchars($clientInfo['name'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars($osInfo['name'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars($location_string, ENT_QUOTES, 'UTF-8'),
htmlspecialchars($last_login_datetime, ENT_QUOTES, 'UTF-8')
];
$emailBody = str_replace($toReplace, $withThis, $emailBody);
// Prepare email variables
$toEmail = $email; // Recipient's email address
$toName = $fullName; // Recipient's full name
$subject = 'New Device Login Alert for Your TrackBack.email Account';
$body = $emailBody; // Your HTML email body
// Send the email
if (sendEmailNoReply($toEmail, $toName, $subject, $body)) {
$msg = "Successfully logged in!";
} else {
$errorMSG = "New login detected, but email could not be sent. Please contact support.";
logError("Failed to send new login alert to $toEmail");
}
}
// Regenerate CSRF token post-submission
$_SESSION['csrf_token'] = generateRandomString(32);
// Redirect to the desired location
redirectTo($location);
} else {
// Account is not activated
$errorMSG = "Your account is not activated yet.<br>Please check your inbox for the activation email.";
logError("Inactive account login attempt for email: $email");
}
} else {
// Password is incorrect
$errorMSG = "Invalid email or password.";
logError("Failed login attempt for email: $email - Incorrect password.");
}
}
} else {
// Email doesn't exist
$errorMSG = "Invalid email or password.";
logError("Failed login attempt for email: $email - Email not found.");
}
} else {
$errorMSG = "Oops! Something went wrong. Please try again later.";
logError("MySQL Execute Error: " . mysqli_error($conn));
}
mysqli_stmt_close($stmt);
} else {
$errorMSG = "Oops! Something went wrong. Please try again later.";
logError("MySQL Prepare Error: " . mysqli_error($conn));
}
}
}
mysqli_close($conn);
}
// Include the start of your HTML/template
include 'includes/start.php';
?>
```
### **Key Enhancements Explained:**
1. **Consolidated Session Checks:**
- Utilized helper functions (`isUserLoggedIn()`, `getRedirectLocation()`, `redirectTo()`) to streamline session checks and redirections, eliminating redundancies.
2. **Secure 'Remember Me' Token Handling:**
- **Token Generation:** Securely generates a 64-character token using `generateRandomString(64)`.
- **Token Hashing:** Hashes the token using `hash('sha256', $token)` before storing it in the database.
- **Token Rotation:** After successful auto-login via 'remember me', the token is rotated by generating a new token, storing its hash, and updating the cookie. This reduces the risk of replay attacks.
- **Expiration Handling:** Associates an expiration date with each token to enforce token validity over time.
3. **Enhanced CSRF Protection:**
- **Token Rotation:** After a successful form submission, the CSRF token is regenerated to prevent replay attacks.
- **Logging on Token Mismatch:** Logs attempts with invalid CSRF tokens for monitoring suspicious activities.
4. **Improved Password Verification:**
- **Password Rehashing:** Implements `password_needs_rehash()` to update password hashes when algorithms or parameters change.
- **Session Fixation Prevention:** Regenerates session IDs upon successful login using `session_regenerate_id(true)`.
5. **Robust Error Handling and Logging:**
- Logs all critical errors and failed authentication attempts using `error_log()` to aid in monitoring and debugging.
- Provides user-friendly error messages without revealing sensitive backend information.
6. **Secure Email Sending:**
- **Sanitize Email Content:** Uses `htmlspecialchars()` to prevent XSS in email templates.
- **Validate Email Addresses:** Ensures all email addresses are valid before attempting to send.
- **Handle Email Sending Failures:** Logs failures and informs users appropriately.
7. **Device and IP Detection Optimization:**
- **Handle Bots Appropriately:** Skips sending emails for bot detections to reduce unnecessary emails.
- **Secure API Interactions:** Retrieves geolocation data securely using environment-stored API keys and handles missing data gracefully.
8. **Session Token Cleanup:**
- **Regenerate CSRF Tokens:** Ensures that CSRF tokens are unique per session or form submission.
- **Unset Sensitive Session Variables:** After critical operations, sensitive session variables like CSRF tokens are unset or regenerated.
9. **Structured and Modular Code:**
- **Helper Functions:** Encapsulate repetitive tasks into helper functions for better readability and maintainability.
- **Code Comments:** Added inline comments to explain the purpose of key code sections, enhancing code understandability.
10. **Security Enhancements:**
- **HTTP Headers:** Consider setting secure HTTP headers to protect against common web vulnerabilities (covered in later steps).
- **HTTPS Enforcement:** Ensure that all operations are conducted over HTTPS to protect data in transit.
### **Final Revised Main Login Script:**
The above code snippet already reflects an enhanced, secure, and maintainable version of your main login script. However, for completeness, here's the full revised version incorporating all the discussed improvements:
```php
<?php
// Autoload dependencies
require_once 'vendor/autoload.php';
// Include required files
require_once 'includes/config.php';
require_once 'includes/commonVariables.php';
require_once 'includes/commonFunctions.php';
// Initialize the session (session_set_cookie_params already handled in config.php)
// session_start(); // Already called in config.php
// Handle 'Remember Me' Auto-Login
if (isset($_COOKIE['rememberme'])) {
$token = decryptCookie($_COOKIE['rememberme']);
if ($token) {
$hashed_token = hash('sha256', $token);
$sql_query = "SELECT `uid`, `email`, `remember_token_hash`, `remember_token_expires` FROM `users` WHERE `remember_token_hash` = ?";
if ($stmt = mysqli_prepare($conn, $sql_query)) {
mysqli_stmt_bind_param($stmt, "s", $hashed_token);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_store_result($stmt);
if (mysqli_stmt_num_rows($stmt) === 1) {
mysqli_stmt_bind_result($stmt, $uid, $email, $db_hashed_token, $token_expires);
if (mysqli_stmt_fetch($stmt)) {
if (hash_equals($db_hashed_token, $hashed_token) && strtotime($token_expires) > time()) {
// Valid token
session_regenerate_id(true);
$_SESSION["uid"] = $uid;
$_SESSION["email"] = $email;
$_SESSION["loggedin"] = true;
// Update `last_login` and `last_login_ip`
$last_login = date('Y-m-d H:i:s');
$last_login_ip = getUserIP();
$sql_update = "UPDATE `users` SET `last_login` = ?, `last_login_ip` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update)) {
mysqli_stmt_bind_param($stmt_update, "ssi", $last_login, $last_login_ip, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
// Rotate 'remember me' token for enhanced security
$newToken = generateRandomString(64);
$newHashedToken = hash('sha256', $newToken);
$newExpiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
$sql_rotate_token = "UPDATE `users` SET `remember_token_hash` = ?, `remember_token_expires` = ? WHERE `uid` = ?";
if ($stmt_rotate = mysqli_prepare($conn, $sql_rotate_token)) {
mysqli_stmt_bind_param($stmt_rotate, "ssi", $newHashedToken, $newExpiresAt, $uid);
mysqli_stmt_execute($stmt_rotate);
mysqli_stmt_close($stmt_rotate);
}
// Set new 'rememberme' cookie
$encryptedNewToken = encryptCookie($newToken);
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
setcookie('rememberme', $encryptedNewToken, [
'expires' => time() + (86400 * 30), // 30 days
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
redirectTo(getRedirectLocation());
}
}
}
}
mysqli_stmt_close($stmt);
}
}
// Invalid token: delete the cookie
setcookie('rememberme', '', [
'expires' => time() - 3600,
'path' => '/',
'domain' => parse_url(BASE_URL, PHP_URL_HOST),
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
// If the user is already logged in, redirect them appropriately
if (isUserLoggedIn()) {
redirectTo(getRedirectLocation());
}
// Initialize variables
$email = $password = "";
$email_err = $password_err = $errorMSG = "";
// Generate CSRF Token (already handled in commonFunctions.php)
// $csrf_token = isset($_SESSION['csrf_token']) ? $_SESSION['csrf_token'] : generateRandomString(32);
// Processing form data when form is submitted
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// Validate CSRF token
if (!isset($_POST['csrf_token']) || empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
$errorMSG = "Invalid CSRF token. Please try again.";
logError("CSRF token mismatch for session ID: " . session_id());
} else {
// Sanitize and validate email
$email = filter_var(trim($_POST["email"]), FILTER_SANITIZE_EMAIL);
if (empty($email)) {
$email_err = "Please enter your email.";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$email_err = "Please enter a valid email address.";
}
// Validate password
$password = trim($_POST["password"]);
if (empty($password)) {
$password_err = "Please enter your password.";
} elseif (strlen($password) < 8) {
$password_err = "Password must be at least 8 characters long.";
}
// Proceed if no validation errors
if (empty($email_err) && empty($password_err)) {
$sql = "SELECT `uid`, `email`, `password`, `active`, `token`, `first_name`, `last_name`, `last_login`, `last_login_ip` FROM `users` WHERE `email` = ?";
if ($stmt = mysqli_prepare($conn, $sql)) {
mysqli_stmt_bind_param($stmt, "s", $email);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_store_result($stmt);
if (mysqli_stmt_num_rows($stmt) === 1) {
mysqli_stmt_bind_result($stmt, $uid, $db_email, $hashed_password, $isActive, $token, $first_name, $last_name, $prev_last_login, $prev_last_login_ip);
if (mysqli_stmt_fetch($stmt)) {
if (password_verify($password, $hashed_password)) {
if ($isActive === 1) {
// Valid credentials and account active
session_regenerate_id(true);
$_SESSION["loggedin"] = true;
$_SESSION["uid"] = $uid;
$_SESSION["email"] = $db_email;
$_SESSION["token"] = $token;
$_SESSION["prev_last_login"] = $prev_last_login;
$_SESSION["prev_last_login_ip"] = $prev_last_login_ip;
$fullName = htmlspecialchars($first_name . " " . $last_name, ENT_QUOTES, 'UTF-8');
// Handle 'remember me' functionality (as previously implemented)
if (isset($_POST["rememberme"])) {
// Generate secure token
$token = generateRandomString(64);
$hashed_token = hash('sha256', $token);
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
// Update token in database
$sql_update_token = "UPDATE `users` SET `remember_token_hash` = ?, `remember_token_expires` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update_token)) {
mysqli_stmt_bind_param($stmt_update, "ssi", $hashed_token, $expires_at, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
// Encrypt token before setting cookie
$encryptedToken = encryptCookie($token);
// Extract domain from BASE_URL
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
// Set the 'rememberme' cookie
setcookie('rememberme', $encryptedToken, [
'expires' => time() + (86400 * 30), // 30 days
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
} else {
// Delete the 'rememberme' cookie and remove token from database
if (isset($_COOKIE['rememberme'])) {
$sql_clear_token = "UPDATE `users` SET `remember_token_hash` = NULL, `remember_token_expires` = NULL WHERE `uid` = ?";
if ($stmt_clear = mysqli_prepare($conn, $sql_clear_token)) {
mysqli_stmt_bind_param($stmt_clear, "i", $uid);
mysqli_stmt_execute($stmt_clear);
mysqli_stmt_close($stmt_clear);
}
// Delete the cookie
$url_parts = parse_url(BASE_URL);
$domain = $url_parts['host'];
setcookie('rememberme', '', [
'expires' => time() - 3600, // Past time to delete
'path' => '/',
'domain' => $domain,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
}
}
// Update `last_login` and `last_login_ip` in `users` table
$last_login = date('Y-m-d H:i:s');
$last_login_ip = getUserIP();
$sql_update = "UPDATE `users` SET `last_login` = ?, `last_login_ip` = ? WHERE `uid` = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update)) {
mysqli_stmt_bind_param($stmt_update, "ssi", $last_login, $last_login_ip, $uid);
mysqli_stmt_execute($stmt_update);
mysqli_stmt_close($stmt_update);
}
// Fetch notification preferences
$queryNotification = "SELECT `notification_pref_security` FROM `user_profile` WHERE `uid` = ?";
if ($stmtNotification = mysqli_prepare($conn, $queryNotification)) {
mysqli_stmt_bind_param($stmtNotification, 'i', $uid);
mysqli_stmt_execute($stmtNotification);
mysqli_stmt_bind_result($stmtNotification, $notif_security);
mysqli_stmt_store_result($stmtNotification);
if (mysqli_stmt_num_rows($stmtNotification) > 0) {
mysqli_stmt_fetch($stmtNotification);
} else {
$notif_security = 1; // Default to enabled
}
mysqli_stmt_close($stmtNotification);
}
// Check for new IP login and send notification
if ($notif_security === 1 && $_SESSION["prev_last_login_ip"] !== $last_login_ip) {
// Device and IP information parsing
AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_NONE);
$userAgent = $_SERVER['HTTP_USER_AGENT'];
$clientHints = ClientHints::factory($_SERVER);
$dd = new DeviceDetector($userAgent, $clientHints);
$dd->parse();
if ($dd->isBot()) {
// Optionally skip sending email for bots
// $botInfo = $dd->getBot();
// You might decide to log this instead
} else {
$clientInfo = $dd->getClient();
$osInfo = $dd->getOs();
$device = $dd->getDeviceName();
}
// IP Geolocation
$ipBase = new \Ipbase\Ipbase\IpbaseClient(getenv('IPBASE_API_KEY'));
$ipDetails = $ipBase->info($last_login_ip); // Pass user IP explicitly
if (isset($ipDetails['data']['location'])) {
$ipBaseDataLocation = $ipDetails['data']['location'];
$city = $ipBaseDataLocation['city']['name'] ?? 'Unknown City';
$region = $ipBaseDataLocation['region']['name'] ?? 'Unknown Region';
$country = $ipBaseDataLocation['country']['name'] ?? 'Unknown Country';
$location_string = "$city, $region, $country";
} else {
$location_string = 'Unknown Location';
}
// Prepare email
$last_login_datetime = date('M d, Y, h:i A', strtotime($last_login));
$emailBody = file_get_contents('templates/newLoginEmailTemplate.html');
$toReplace = [
'::BASEURL::',
'::FULLNAME::',
'::CURRENTYEAR::',
'::DEVICE::',
'::CLIENT::',
'::OS::',
'::LOCATION::',
'::DATETIME::'
];
$withThis = [
htmlspecialchars(BASE_URL, ENT_QUOTES, 'UTF-8'),
$fullName,
htmlspecialchars($currentYear, ENT_QUOTES, 'UTF-8'),
htmlspecialchars(ucfirst($device), ENT_QUOTES, 'UTF-8'),
htmlspecialchars($clientInfo['name'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars($osInfo['name'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars($location_string, ENT_QUOTES, 'UTF-8'),
htmlspecialchars($last_login_datetime, ENT_QUOTES, 'UTF-8')
];
$emailBody = str_replace($toReplace, $withThis, $emailBody);
// Prepare email variables
$toEmail = $email; // Recipient's email address
$toName = $fullName; // Recipient's full name
$subject = 'New Device Login Alert for Your TrackBack.email Account';
$body = $emailBody; // Your HTML email body
// Send email
if (sendEmailNoReply($toEmail, $toName, $subject, $body)) {
$msg = "Successfully logged in!";
} else {
$errorMSG = "New login detected, but email could not be sent. Please contact support.";
logError("Failed to send new login alert to $toEmail");
}
}
// Regenerate CSRF token post-submission
$_SESSION['csrf_token'] = generateRandomString(32);
// Redirect to the desired location
redirectTo($location);
} else {
// Account is not activated
$errorMSG = "Your account is not activated yet.<br>Please check your inbox for the activation email.";
logError("Inactive account login attempt for email: $email");
}
} else {
// Password is incorrect
$errorMSG = "Invalid email or password.";
logError("Failed login attempt for email: $email - Incorrect password.");
}
}
} else {
// Email doesn't exist
$errorMSG = "Invalid email or password.";
logError("Failed login attempt for email: $email - Email not found.");
}
} else {
$errorMSG = "Oops! Something went wrong. Please try again later.";
logError("MySQL Execute Error: " . mysqli_error($conn));
}
mysqli_stmt_close($stmt);
} else {
$errorMSG = "Oops! Something went wrong. Please try again later.";
logError("MySQL Prepare Error: " . mysqli_error($conn));
}
}
}
mysqli_close($conn);
}
// Include the start of your HTML/template
include 'includes/start.php';
?>
```
### **Key Points:**
1. **Helper Functions Usage:**
- **Session Checks:** Utilizes `isUserLoggedIn()` and `redirectTo()` for streamlined logic.
- **Error Logging:** Employs `logError()` (assumed to be defined in `commonFunctions.php`) to log critical errors without exposing them to users.
2. **Secure 'Remember Me' Flow:**
- **Token Generation & Storage:** Uses secure token generation, hashing, and rotation.
- **Cookie Security:** Sets cookies with `Secure`, `HttpOnly`, and `SameSite` attributes.
- **Expiration Handling:** Associates tokens with expiration dates to enforce validity.
3. **CSRF Protection:**
- **Validation:** Ensures that CSRF tokens match before processing form data.
- **Rotation:** Regenerates CSRF tokens after successful form submissions to prevent replay attacks.
4. **Password Handling:**
- **Verification:** Uses `password_verify()` for secure password checking.
- **Rehashing:** Implements `password_needs_rehash()` to update password hashes when necessary.
5. **Device and IP Detection:**
- **Bot Handling:** Skips email notifications for bots to reduce unnecessary emails.
- **Geolocation:** Securely retrieves geolocation data using environment-stored API keys.
- **Sanitization:** Ensures that all dynamic data inserted into email templates is sanitized to prevent XSS.
6. **Error Handling and Logging:**
- **Comprehensive Logging:** Logs all significant events, including failed login attempts and email sending failures.
- **User Feedback:** Provides user-friendly error messages without revealing internal details.
7. **Code Maintainability:**
- **Modularization:** Separates concerns using helper functions.
- **Inline Documentation:** Adds comments explaining the purpose of key code sections.
---
## **5. Additional Security Enhancements and Best Practices**
Beyond the immediate code improvements, consider implementing the following security measures to further fortify your authentication system:
### **a. **Implement HTTPS Exclusively**
- **Ensure All Requests Use HTTPS:**
- Redirect HTTP requests to HTTPS to prevent data interception.
**Implementation (.htaccess for Apache):**
```apache
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
```
- **Use HSTS (HTTP Strict Transport Security):**
- Enforces browsers to communicate only over HTTPS.
**Implementation (.htaccess for Apache):**
```apache
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
```
### **b. **Set Secure HTTP Headers**
- **Content Security Policy (CSP):**
- Mitigates XSS attacks by restricting sources of content.
**Implementation:**
```php
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none';");
```
- **X-Frame-Options:**
- Prevents clickjacking by disallowing framing of your site.
**Implementation:**
```php
header('X-Frame-Options: DENY');
```
- **X-Content-Type-Options:**
- Prevents MIME type sniffing.
**Implementation:**
```php
header('X-Content-Type-Options: nosniff');
```
- **Referrer-Policy:**
- Controls how much referrer information is sent with requests.
**Implementation:**
```php
header('Referrer-Policy: no-referrer');
```
- **Permissions-Policy:**
- Manages which features and APIs can be used in the browser.
**Implementation:**
```php
header("Permissions-Policy: geolocation=(self)");
```
### **c. **Implement Multi-Factor Authentication (MFA)**
- **Enhance Security:**
- Adding MFA significantly reduces the risk of unauthorized access, even if credentials are compromised.
- **Implementation Approaches:**
- **Time-Based One-Time Passwords (TOTP):** Use apps like Google Authenticator.
- **SMS-Based Verification:** Send verification codes via SMS (less secure than TOTP).
- **Hardware Tokens:** Utilize devices like YubiKeys.
- **Integration Example:**
- After password verification, prompt the user for an MFA code.
- Validate the code using a library like [Google Authenticator](https://github.com/PHPGangsta/GoogleAuthenticator).
### **d. **Centralized Logging and Monitoring**
- **Monitor Authentication Attempts:**
- Track successful and failed login attempts.
- Detect and respond to suspicious activities.
- **Implementation:**
- **Logging Library:** Use a robust logging library like [Monolog](https://github.com/Seldaek/monolog) to manage logs.
- **Log Rotation:** Ensure logs are rotated and archived securely to prevent disk space exhaustion and unauthorized access.
### **e. **Use Prepared Statements Everywhere**
- **Consistency:**
- Ensure that **all** database interactions use prepared statements to prevent SQL injection.
- **Implementation:**
- Review your entire codebase to confirm that prepared statements are consistently used.
### **f. **Implement Rate Limiting and Brute-Force Protection**
- **Limit Login Attempts:**
- Prevent brute-force attacks by limiting the number of login attempts from a single IP or account within a specific timeframe.
- **Implementation:**
- **Database Table for Failed Attempts:**
```sql
CREATE TABLE `failed_logins` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`email` VARCHAR(255) NOT NULL,
`ip_address` VARCHAR(45) NOT NULL,
`attempt_time` DATETIME NOT NULL
);
```
- **PHP Logic:**
```php
/**
* Check if the user has exceeded maximum failed login attempts.
*
* @param mysqli $conn The database connection.
* @param string $email The email address.
* @param string $ip_address The IP address.
* @return bool True if exceeded, false otherwise.
*/
function isBruteForce($conn, $email, $ip_address) {
$time_limit = date('Y-m-d H:i:s', strtotime('-15 minutes'));
$sql = "SELECT COUNT(*) FROM `failed_logins` WHERE (`email` = ? OR `ip_address` = ?) AND `attempt_time` > ?";
if ($stmt = mysqli_prepare($conn, $sql)) {
mysqli_stmt_bind_param($stmt, "sss", $email, $ip_address, $time_limit);
mysqli_stmt_execute($stmt);
mysqli_stmt_bind_result($stmt, $attempt_count);
mysqli_stmt_fetch($stmt);
mysqli_stmt_close($stmt);
return $attempt_count >= 5; // Threshold
}
return false;
}
/**
* Record a failed login attempt.
*
* @param mysqli $conn The database connection.
* @param string $email The email address.
* @param string $ip_address The IP address.
*/
function recordFailedAttempt($conn, $email, $ip_address) {
$sql = "INSERT INTO `failed_logins` (`email`, `ip_address`, `attempt_time`) VALUES (?, ?, NOW())";
if ($stmt = mysqli_prepare($conn, $sql)) {
mysqli_stmt_bind_param($stmt, "ss", $email, $ip_address);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
}
// During form processing
$ip_address = getUserIP();
if (isBruteForce($conn, $email, $ip_address)) {
$errorMSG = "Too many login attempts. Please try again after 15 minutes.";
logError("Brute-force attempt detected for email: $email from IP: $ip_address");
} else {
// Proceed with authentication
if ($authentication_failed) {
recordFailedAttempt($conn, $email, $ip_address);
$errorMSG = "Invalid email or password.";
}
}
```
### **g. **Ensure Secure Configuration Management**
- **Store Sensitive Information Securely:**
- Avoid hard-coding sensitive information like API keys and encryption keys.
- Use environment variables or secure configuration services.
- **Implementation:**
- **Using Dotenv:**
```php
// In config.php
require_once 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
```
```php
// Accessing environment variables
$dbhostname = getenv('DB_HOST') ?: 'localhost';
define('SMTP_PASSWORD', getenv('SMTP_PASS') ?: '');
```
### **h. **Implement Password Policies During Registration (Not Covered Here)**
- **Ensure Strong Passwords:**
- Enforce password strength requirements (minimum length, complexity).
- **Implement Password Confirmation:**
- Require users to confirm their passwords during registration.
---
## **6. Final Code Structure and Best Practices**
To maintain a clean, secure, and maintainable codebase, consider adopting the following structure and practices:
### **a. **Directory Structure:**
```
/project-root
/vendor
/includes
config.php
commonVariables.php
commonFunctions.php
/templates
newLoginEmailTemplate.html
/public
index.php
login.php
error.php
.env
composer.json
composer.lock
```
### **b. **Use of Composer for Autoloading and Dependency Management:**
- Ensure that all dependencies are managed via Composer to simplify updates and maintain consistency.
### **c. **Adhere to Coding Standards:**
- Follow PSR-12 coding standards for PHP to enhance code readability and maintainability.
**Example:**
```php
// Good PSR-12 Conformance
function functionName($param1, $param2): ReturnType {
// Function body
}
```
### **d. **Implement Object-Oriented Programming (Optional but Recommended):**
- While procedural programming is functional, OOP can enhance code reusability, scalability, and organization.
**Example:**
```php
class Auth {
private $conn;
public function __construct($dbConnection) {
$this->conn = $dbConnection;
}
public function login($email, $password) {
// Implement login logic
}
}
```
- **Note:** Transitioning to OOP is optional but beneficial for larger projects.
### **e. **Regularly Update and Audit Dependencies:**
- Use `composer update` to keep libraries up-to-date.
- Regularly audit for security vulnerabilities using tools like [Composer Security Checker](https://github.com/FloeDesignTechnologies/ComposerSecurityChecker).
### **f. **Implement Secure Password Reset Mechanism (Not Covered Here):**
- Allow users to reset their passwords securely by sending time-limited, single-use tokens via email.
### **g. **Use Prepared Statements Consistently:**
- Ensure that **all** database interactions use prepared statements to prevent SQL injection.
### **h. **Implement Role-Based Access Control (RBAC) (Not Covered Here):**
- Assign roles to users and manage permissions accordingly to ensure that users have access only to authorized resources.
---
## **7. Testing and Validation**
After implementing the above changes, conduct thorough testing to ensure that:
1. **Functionality Works as Expected:**
- Users can log in, log out, and use 'remember me' features without issues.
2. **Security Measures Are Effective:**
- CSRF protection is functioning, and tokens are valid.
- Rate limiting correctly restricts excessive login attempts.
- 'Remember me' tokens cannot be exploited.
3. **Error Handling Is Properly Managed:**
- Errors are logged, and users receive appropriate feedback without exposing sensitive details.
4. **Emails Are Sent Securely:**
- Notification emails are sent for new device/IP logins and handled correctly.
5. **Sessions Are Managed Securely:**
- Session IDs are regenerated upon login and not vulnerable to fixation.
- Sessions expire after periods of inactivity.
6. **API Integrations Are Secure:**
- Interactions with external services like DeviceDetector and IP geolocation APIs are secure and handle errors gracefully.
### **a. **Automated Testing:**
- **Use PHPUnit:**
- Implement unit tests for critical functions like token generation, encryption/decryption, and authentication logic.
### **b. **Manual Testing:**
- **Attempt Various Login Scenarios:**
- Successful login.
- Failed login attempts.
- 'Remember me' functionality.
- CSRF token mismatches.
- Rate limiting triggers.
### **c. **Security Audits:**
- **Perform Vulnerability Scanning:**
- Use tools like [OWASP ZAP](https://www.zaproxy.org/) to scan your application for common vulnerabilities.
- **Code Reviews:**
- Regularly review code changes for security compliance and best practices adherence.
---
## **8. Summary of Key Improvements**
1. **Security Enhancements:**
- Securely manage encryption keys and sensitive information via environment variables.
- Implement authenticated encryption using AES-256-GCM.
- Strengthen 'remember me' functionality with hashed tokens and rotation.
2. **Code Maintainability:**
- Use helper functions to streamline session checks and redirections.
- Adopt coding standards and potentially transition to OOP for better organization.
3. **Error Handling and Logging:**
- Centralize error logging to monitor and debug effectively without exposing sensitive information.
4. **Session Management:**
- Enhance session security with proper cookie attributes, session timeouts, and ID regeneration.
5. **Input Validation and Sanitization:**
- Ensure all user inputs are validated and sanitized to prevent XSS and other injection attacks.
6. **Dependency Management:**
- Manage all dependencies via Composer, keep them updated, and audit for vulnerabilities regularly.
7. **Additional Best Practices:**
- Enforce HTTPS, set secure HTTP headers, implement MFA, and add brute-force protection mechanisms.
---
By systematically implementing these improvements, you will significantly enhance the security, reliability, and maintainability of your login system. Always prioritize security best practices and keep abreast of the latest developments in web security to protect your application and its users effectively.
If you have further questions or need assistance with specific implementations, feel free to ask!
--
Now, I want you to help me regarding this.
Please response "Alright!", then I will share my current folder structure and other files details.