Skip to main content

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:
  1. Modern Bcrypt ($2y$...)
  2. Prefixed Bcrypt ($wp$2y$...)
  3. 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:
KeyTypeDescription
user_idintWordPress/Database user ID
user_loginstringUsername (for logging)
display_namestringFull name for UI display
rolestringUser role (administrator/editor/subscriber)
logged_inbooleanAuthentication 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:
RoleAccess LevelTypical Use
administratorFull accessSuper admin, system configuration
editorContent managementContent creators, moderators
subscriberLimited accessEnd 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
  1. Session Fixation Protection:
    session_regenerate_id(true);
    
  2. HTTPS Enforcement:
    if ($_SERVER['HTTPS'] !== 'on') {
        header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
        exit;
    }
    
  3. 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'
    ]);
    
  4. Password Strength Requirements:
    if (strlen($password) < 12) {
        $error = 'Password must be at least 12 characters';
    }
    

Next Steps

Build docs developers (and LLMs) love