Overview
Dashboard Dilemas implements a dual-mode authentication system that can operate either integrated with WordPress or as a standalone solution. This flexibility allows deployment in various environments while maintaining consistent security standards.
Authentication Modes
The system automatically detects the available authentication method at runtime:
if (load_wordpress()) {
// WordPress Integration Mode
$user = wp_authenticate($username, $password);
} else {
// Standalone PDO Mode
$user = authenticate_local($username, $password);
}
WordPress Integration Mode
When WordPress is available (defined WP_PATH):
- Uses
wp_authenticate() for credential validation
- Leverages WordPress role system (
administrator, editor, subscriber)
- Password resets via
get_password_reset_key()
- Email delivery through
wp_mail()
Configuration:
// includes/config.php
define('WP_PATH', '/path/to/wordpress');
Standalone Mode
Fallback authentication when WordPress is unavailable:
- Direct PDO queries against
de_users table
- Multi-method password verification (Bcrypt, Phpass)
- Role management via
de_usermeta table
- Native PHP
mail() function for emails
The system gracefully degrades from WordPress mode to standalone mode, ensuring authentication always works regardless of WordPress availability.
Login Process
The login flow follows these steps:
1. Rate Limiting Check
Before authentication, the system checks for brute-force attempts:
if (rl_is_blocked()) {
$mins = ceil(rl_remaining_seconds() / 60);
$error = "Demasiados intentos fallidos. Espera {$mins} min.";
return false;
}
Rate Limiting Parameters:
- Maximum Attempts: 5 failed logins
- Lockout Window: 15 minutes (900 seconds)
- Scope: IP address-based
- Storage: File-based in
/tmp/dilemas_rl/
Rate limiting data is stored with MD5-hashed IP addresses to prevent directory enumeration attacks.
2. Credential Validation
WordPress Mode
$user = wp_authenticate($username, $password);
if (is_wp_error($user)) {
// Authentication failed
return false;
}
// Extract role from WordPress capabilities
$role = 'subscriber';
if (!empty($user->roles) && is_array($user->roles)) {
$role = reset($user->roles);
}
Standalone Mode
// 1. Find user by username or email
$stmt = $pdo->prepare(
"SELECT * FROM de_users WHERE user_login = :login OR user_email = :email"
);
$stmt->execute(['login' => $username, 'email' => $username]);
$user = $stmt->fetch();
if (!$user) {
return false; // User not found
}
// 2. Verify password (multiple methods)
$authenticated = verify_password($password, $user['user_pass']);
3. Password Verification
The system supports multiple password hashing formats:
function verify_password($password, $hash) {
// Method 1: Standard Bcrypt
if (password_verify($password, $hash)) {
return true;
}
// Method 2: Bcrypt with $wp$ prefix (legacy)
if (str_starts_with($hash, '$wp$')) {
$clean_hash = str_replace('$wp$', '', $hash);
if (password_verify($password, $clean_hash)) {
return true;
}
}
// Method 3: WordPress Phpass
$hasher = new PasswordHash(8, true);
return $hasher->CheckPassword($password, $hash);
}
Hash Format Priority:
- Modern Bcrypt (
$2y$...)
- Prefixed Bcrypt (
$wp$2y$...)
- WordPress Portable Phpass (
$P$...)
The multi-method approach ensures compatibility with passwords created in different environments (native WordPress, WP-CLI, custom admin panels).
4. Role Detection
User roles determine access levels throughout the application:
// Check for capabilities in de_usermeta
$stmt = $pdo->prepare(
"SELECT meta_value FROM de_usermeta
WHERE user_id = :id AND meta_key = :key"
);
// Try de_capabilities first, then wp_capabilities
$keys_to_check = ['de_capabilities', 'wp_capabilities'];
foreach ($keys_to_check as $key) {
$stmt->execute(['id' => $user['ID'], 'key' => $key]);
$meta_val = $stmt->fetchColumn();
if ($meta_val) {
$capabilities = unserialize($meta_val);
if (isset($capabilities['administrator'])) {
$role = 'administrator';
} elseif (isset($capabilities['editor'])) {
$role = 'editor';
}
break;
}
}
Serialized Capabilities Example:
// Database value:
a:1:{s:13:"administrator";b:1;}
// Unserializes to:
['administrator' => true]
5. Session Initialization
Successful authentication creates a PHP session:
$_SESSION['user_id'] = $user['ID'];
$_SESSION['user_login'] = $user['user_login'];
$_SESSION['display_name'] = $user['display_name'];
$_SESSION['role'] = $role; // administrator, editor, subscriber
$_SESSION['logged_in'] = true;
Session Management
Session Variables
The application maintains these session keys:
| Key | Type | Description |
|---|
user_id | int | WordPress/Database user ID |
user_login | string | Username (for logging) |
display_name | string | Full name for UI display |
role | string | User role (administrator/editor/subscriber) |
logged_in | boolean | Authentication flag |
Session Security
Session handling includes security measures:
session_start();
// Check authentication
function is_logged_in() {
return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
}
// Verify admin access
function check_is_admin() {
return is_logged_in() && $_SESSION['role'] === 'administrator';
}
// Protected page guard
function require_login() {
if (!is_logged_in()) {
header('Location: ' . SITE_URL . '/login');
exit;
}
}
Session Timeout
The application tracks user activity:
// Update last active timestamp
UPDATE de_app_users
SET lastActive = NOW()
WHERE id = :user_id;
While PHP sessions have native timeout, the lastActive field in de_app_users provides application-level activity tracking for analytics.
Password Reset Flow
The password recovery system uses secure token-based authentication:
1. Request Reset
User initiates recovery via email or username:
function send_password_reset($user_or_email) {
// Find user
$stmt = $pdo->prepare(
"SELECT * FROM de_users
WHERE user_login = :input OR user_email = :email"
);
$stmt->execute(['input' => $user_or_email, 'email' => $user_or_email]);
$user = $stmt->fetch();
if (!$user) {
return false;
}
// Generate secure token
$key = bin2hex(random_bytes(20)); // 40 character hex string
// Store token
$stmt = $pdo->prepare(
"UPDATE de_users SET user_activation_key = :key WHERE ID = :id"
);
$stmt->execute(['key' => $key, 'id' => $user['ID']]);
// Send email
$link = SITE_URL . "/reset-password?key=$key&login=" . urlencode($user['user_login']);
send_recovery_email($user['user_email'], $link);
return $link;
}
Token Characteristics:
- Generation:
random_bytes(20) - cryptographically secure
- Format: 40 character hexadecimal string
- Storage:
user_activation_key column in de_users
- Lifetime: No expiration (single-use)
2. Email Delivery
Styled HTML recovery email:
function send_recovery_email($to, $link) {
$subject = "Recuperacion de Contraseña - Dilemas Eticos";
$message = "
<html>
<head><style>/* Inline CSS styles */</style></head>
<body>
<div class='email-container'>
<div class='content'>
<div class='logo'>FGE Dilemas Eticos</div>
<h2>Recuperar Acceso</h2>
<p>Has solicitado restablecer tu contraseña...</p>
<a href='{$link}' class='button'>Restablecer Contraseña</a>
</div>
</div>
</body>
</html>";
if (load_wordpress()) {
return wp_mail($to, $subject, $message, ['Content-Type: text/html']);
} else {
$headers = "Content-type:text/html;charset=UTF-8\r\n";
return mail($to, $subject, $message, $headers);
}
}
3. Token Verification
Validate reset link parameters:
function verify_reset_key($key, $login) {
global $pdo;
$stmt = $pdo->prepare(
"SELECT * FROM de_users
WHERE user_login = :login AND user_activation_key = :key"
);
$stmt->execute(['login' => $login, 'key' => $key]);
return $stmt->fetch(); // Returns user if valid, false otherwise
}
4. Password Update
Set new password and clear reset token:
function update_user_password($login, $new_password) {
if (load_wordpress()) {
$user = get_user_by('login', $login);
wp_set_password($new_password, $user->ID);
// wp_set_password clears activation key automatically
return true;
}
// Standalone mode
global $pdo;
$hasher = new PasswordHash(8, true);
$hashed_password = $hasher->HashPassword($new_password);
$stmt = $pdo->prepare(
"UPDATE de_users
SET user_pass = :pass, user_activation_key = ''
WHERE user_login = :login"
);
return $stmt->execute(['pass' => $hashed_password, 'login' => $login]);
}
The user_activation_key field is cleared after successful password reset to prevent token reuse.
Password Hashing
The system uses WordPress Portable PHP Password Hashing Framework (Phpass):
Hash Generation
$hasher = new PasswordHash(8, true);
$hashed = $hasher->HashPassword($plaintext_password);
// Example output:
// $P$B6tVlZHyZ9xF1v2L3wQb4cK5dE6fG7h
Parameters:
- Iteration Rounds: 8 (2^8 = 256 iterations)
- Portable:
true (uses MD5-based hashing for compatibility)
Hash Verification
$hasher = new PasswordHash(8, true);
$is_valid = $hasher->CheckPassword($plaintext, $hash);
Security Characteristics
Strengths:
- Salted hashing (random salt per password)
- Configurable iteration count (default 8 = 256 rounds)
- Portable across PHP versions
- WordPress-compatible
Considerations:
- MD5-based (not as strong as Bcrypt/Argon2)
- Sufficient for low-value targets
- Compatible with legacy WordPress installations
For new installations, consider migrating to Bcrypt (password_hash()) with automatic rehashing on login for improved security.
Rate Limiting
Brute-force protection implementation:
Storage Structure
// File: /tmp/dilemas_rl/[md5_of_ip].json
{
"count": 3,
"first_at": 1709481234
}
Functions
// Check if IP is blocked
function rl_is_blocked(): bool {
$data = rl_get();
// Reset if window expired
if (time() - $data['first_at'] > RL_WINDOW_SECONDS) {
rl_reset();
return false;
}
return $data['count'] >= RL_MAX_ATTEMPTS;
}
// Increment failed attempts
function rl_increment(): void {
$data = rl_get();
// Reset counter if window expired
if (time() - $data['first_at'] > RL_WINDOW_SECONDS) {
$data = ['count' => 0, 'first_at' => time()];
}
$data['count']++;
file_put_contents(rl_file(), json_encode($data), LOCK_EX);
}
// Clear rate limit after successful login
function rl_reset(): void {
@unlink(rl_file());
}
User Feedback
Progressive lockout messaging:
if ($authenticated) {
rl_reset();
// Proceed with login
} else {
rl_increment();
$attempts = rl_get()['count'];
$remaining = RL_MAX_ATTEMPTS - $attempts;
if ($remaining > 0) {
$error = "Usuario o contraseña incorrectos. ({$remaining} intentos restantes)";
} else {
$mins = ceil(RL_WINDOW_SECONDS / 60);
$error = "Cuenta bloqueada temporalmente por {$mins} min.";
}
}
Access Control
Role-Based Permissions
The application recognizes three primary roles:
| Role | Access Level | Typical Use |
|---|
administrator | Full access | Super admin, system configuration |
editor | Content management | Content creators, moderators |
subscriber | Limited access | End users, participants |
Permission Checks
// Require any authenticated user
require_login();
// Require administrator role
if (!check_is_admin()) {
die('Access denied');
}
// Conditional UI rendering
if (get_current_role() === 'administrator') {
// Show admin controls
}
Development Bypass
For development environments only:
// includes/auth.php
$bypass_login = false; // Set to true for development
function require_login() {
global $bypass_login;
if ($bypass_login === true && !is_logged_in()) {
$_SESSION['user_id'] = 0;
$_SESSION['user_login'] = 'dev_admin';
$_SESSION['display_name'] = 'Developer';
$_SESSION['role'] = 'administrator';
$_SESSION['logged_in'] = true;
return;
}
// Normal authentication required
}
CRITICAL: Never enable $bypass_login = true in production. This completely disables authentication.
Logging and Debugging
Authentication events are logged for troubleshooting:
$debugFile = __DIR__ . '/../auth_debug.log';
$log = function($msg) use ($debugFile) {
file_put_contents(
$debugFile,
date('[Y-m-d H:i:s] ') . $msg . "\n",
FILE_APPEND
);
};
$log("Attempting login for: $username using WordPress Core");
$log("WordPress Auth Success. User ID: " . $user->ID);
$log("Role identified: $role");
Log File Location: /workspace/source/auth_debug.log
Sample Log Entries:
[2026-03-03 10:15:23] Attempting login for: admin using WordPress Core
[2026-03-03 10:15:23] WordPress Auth Success. User ID: 1
[2026-03-03 10:15:23] Role identified: administrator
[2026-03-03 10:15:23] Login SUCCESS for user: admin
Security Best Practices
Implemented Protections
- SQL Injection: PDO prepared statements
- Brute Force: Rate limiting with progressive lockout
- Session Hijacking: IP-based rate limiting
- Password Security: Salted hashing with iterations
- XSS Prevention:
htmlspecialchars() on output
- Token Reuse: Clearing activation keys after use
Recommended Enhancements
-
Session Fixation Protection:
session_regenerate_id(true);
-
HTTPS Enforcement:
if ($_SERVER['HTTPS'] !== 'on') {
header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
exit;
}
-
Secure Cookie Flags:
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => $_SERVER['HTTP_HOST'],
'secure' => true, // HTTPS only
'httponly' => true, // No JavaScript access
'samesite' => 'Strict'
]);
-
Password Strength Requirements:
if (strlen($password) < 12) {
$error = 'Password must be at least 12 characters';
}
Next Steps