Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Nieto2020/portalhub/llms.txt

Use this file to discover all available pages before exploring further.

PortalHub uses PHP server-side sessions for all authentication, meaning no JWT tokens or external auth services are required. Every request that reaches a protected endpoint passes through a shared checkAuth() middleware function that validates session state, enforces inactivity timeouts, and handles forced password-change workflows before any business logic runs.

Login Flow

The login endpoint (modules/auth/login.php) accepts a JSON POST body and follows a strict sequence to prevent enumeration, session fixation, and brute-force attacks.
1

Validate Required Fields

The request body must contain correo and password. Missing fields return 400.
2

Look Up User and Verify Password

A prepared PDO statement fetches id_usuario, id_rol, password_hash, estado, and require_password_change by email. The stored hash is verified with PHP’s password_verify(). On failure, a sleep(1) delay is applied before returning 401 to slow brute-force attempts.
3

Check Account Status

If estado !== 'activo' the login is rejected with 403. Inactive accounts cannot authenticate regardless of credential correctness.
4

Regenerate Session ID

session_regenerate_id(true) is called before writing any session data, eliminating session-fixation vulnerabilities.
5

Write Session Variables

Four keys are stored in $_SESSION:
  • id_usuario — numeric user primary key
  • id_rol — role constant (ROL_ADMIN = 1, ROL_ASESOR = 2, ROL_CLIENTE = 3)
  • require_password_change0 or 1
  • ultima_actividadtime() timestamp used for inactivity tracking
6

Generate CSRF Token

A 64-character hex token is created via bin2hex(random_bytes(32)) and stored in $_SESSION['csrf_token']. The token lives server-side in the session; frontends may retrieve it at any time via GET modules/auth/csrf_token.php.
Example login request and response:
{
  "correo": "cliente@empresa.mx",
  "password": "Secreto123!"
}

Session Inactivity Timeout

config.php defines a global constant used by every middleware call:
define('SESSION_TIMEOUT', 1800); // 30 minutes
On every authenticated request, checkAuth() computes how long ago the session was last active and destroys it if the threshold is exceeded:
function checkAuth($actualizarActividad = true) {
    if (!isset($_SESSION['id_usuario'])) {
        sendResponse(401, "No autorizado. Inicie sesión Primero.");
    }

    // Inactivity check
    if (isset($_SESSION['ultima_actividad'])) {
        $inactividad = time() - $_SESSION['ultima_actividad'];
        if ($inactividad > SESSION_TIMEOUT) {
            session_unset();
            session_destroy();
            sendResponse(401, "Sesión expirada por inactividad. Por favor, ingrese de nuevo.");
        }
    }

    // Refresh the activity timestamp
    if ($actualizarActividad) {
        $_SESSION['ultima_actividad'] = time();
    }
}
check_session.php calls checkAuth(false) — it verifies the session is alive without refreshing ultima_actividad, making it safe for polling from the frontend without extending an otherwise-idle session.

Forced Password Change

When require_password_change = 1 is set on a user account (for example, on first login or after an admin reset), every endpoint except the three below is blocked with 403:
  • update_password.php
  • logout.php
  • check_session.php
if (isset($_SESSION['require_password_change']) && $_SESSION['require_password_change'] == 1) {
    $current_script = basename($_SERVER['PHP_SELF']);
    if (
        $current_script !== 'update_password.php' &&
        $current_script !== 'logout.php' &&
        $current_script !== 'check_session.php'
    ) {
        sendResponse(403, "Cambio de contraseña obligatorio", ["require_password_change" => true]);
    }
}
update_password.php requires the CSRF token and validates that both new_password and confirm_password match and are at least 8 characters long. The password is hashed with PASSWORD_BCRYPT. After a successful update, require_password_change is set to 0 in both the database and the live session, and a fresh CSRF token is issued.
$new_password_hash = password_hash($data['new_password'], PASSWORD_BCRYPT);

$stmt = $conexion->prepare(
    "UPDATE usuarios SET password_hash = ?, require_password_change = 0 WHERE id_usuario = ?"
);
$stmt->execute([$new_password_hash, $id_usuario]);

$_SESSION['require_password_change'] = 0;
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
Update password request:
{
  "new_password": "NuevaContraseña!99",
  "confirm_password": "NuevaContraseña!99",
  "csrf_token": "a3f7c2...d91e"
}

CSRF Protection

CSRF tokens are 32 random bytes encoded as a 64-character hexadecimal string:
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
A fresh token can always be retrieved without side effects via GET modules/auth/csrf_token.php. All mutating endpoints that handle sensitive data (such as update_password.php) validate the token using a constant-time comparison:
if (!hash_equals($_SESSION['csrf_token'], $data['csrf_token'])) {
    sendResponse(403, "Token CSRF inválido");
}

Role-Based Access Control

checkRole() extends checkAuth() with an array of allowed role IDs. Any request from a user whose id_rol is not in that array receives 403:
function checkRole(array $allowedRoles) {
    checkAuth();
    if (!in_array($_SESSION['id_rol'], $allowedRoles)) {
        sendResponse(403, "Acceso denegado. Permisos insuficientes.");
    }
}
Role constants defined in config.php:
ConstantValueDescription
ROL_ADMIN1Full administrative access
ROL_ASESOR2Accounting advisor
ROL_CLIENTE3End-client portal user

Logout

logout.php performs a clean session teardown and explicitly expires the session cookie to prevent replay:
session_start();
session_destroy();

if (ini_get("session.use_cookie")) {
    $params = session_get_cookie_params();
    setcookie(
        session_name(), '',
        time() - 4200,
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}
config.php configures PHP’s session cookie with security-first defaults before any session_start() call:
SettingValuePurpose
session.cookie_httponly1Prevents JavaScript access to the session cookie
session.cookie_secureAuto-detectedCookie sent over HTTPS only when HTTPS is active
session.use_only_cookies1Disables session ID in URL query strings
session.cookie_samesiteLaxMitigates cross-site request forgery via cookies
If you deploy PortalHub behind a reverse proxy (e.g., Nginx + SSL termination), ensure the HTTP_X_FORWARDED_PROTO or HTTP_FRONT_END_HTTPS headers are correctly forwarded so cookie_secure is activated automatically.

Build docs developers (and LLMs) love