Autenticación y autorización en PHP: roles, permisos y protección de páginas


En la guía anterior vimos cómo implementar un sistema básico de login y logout con sesiones, cookies, PDO y contraseñas seguras. Ahora vamos a dar el siguiente paso: controlar qué puede hacer cada usuario.

Para eso necesitamos distinguir dos conceptos:

  • autenticación: comprobar quién es el usuario;
  • autorización: decidir qué acciones puede realizar.

Autenticación vs autorización

Cuando un usuario inicia sesión correctamente, queda autenticado.

Pero eso no significa que pueda acceder a cualquier parte del sistema.

Por ejemplo:

  • un usuario común puede ver su perfil;
  • un administrador puede gestionar usuarios;
  • un moderador puede editar contenidos;
  • un invitado no puede acceder a páginas privadas.

Agregar un rol a la tabla de usuarios

Una forma simple de comenzar es agregar una columna rol.

ALTER TABLE usuarios
ADD rol VARCHAR(50) NOT NULL DEFAULT 'usuario';

Por ejemplo, podemos tener:

  • usuario;
  • admin;
  • moderador.

Guardar el rol en la sesión

Después de validar el login, podemos guardar el rol del usuario:

$_SESSION['usuario_id'] = $usuario['id'];
$_SESSION['usuario_nombre'] = $usuario['nombre'];
$_SESSION['usuario_rol'] = $usuario['rol'];

Proteger una página para usuarios autenticados

<?php

session_start();

if (!isset($_SESSION['usuario_id'])) {
    header('Location: login.php');
    exit;
}

?>

Proteger una página solo para administradores

<?php

session_start();

if (
    !isset($_SESSION['usuario_id']) ||
    $_SESSION['usuario_rol'] !== 'admin'
) {
    http_response_code(403);
    exit('Acceso denegado');
}

?>

No alcanza con ocultar botones

Un error muy común es esconder botones en la interfaz y pensar que eso protege la acción.

Por ejemplo:

<?php if ($_SESSION['usuario_rol'] === 'admin'): ?>

    <a href="eliminar_usuario.php?id=15">
        Eliminar usuario
    </a>

<?php endif; ?>

Esto mejora la interfaz, pero no reemplaza el control en el servidor.

La página eliminar_usuario.php también debe verificar que el usuario tenga permisos.

Centralizar la verificación

En lugar de repetir las mismas condiciones en todas las páginas, podemos crear funciones.

<?php

function usuarioAutenticado(): bool
{
    return isset($_SESSION['usuario_id']);
}

function esAdmin(): bool
{
    return isset($_SESSION['usuario_rol'])
        && $_SESSION['usuario_rol'] === 'admin';
}

?>

Crear funciones para exigir permisos

function requireLogin(): void
{
    if (!isset($_SESSION['usuario_id'])) {
        header('Location: login.php');
        exit;
    }
}

function requireAdmin(): void
{
    requireLogin();

    if ($_SESSION['usuario_rol'] !== 'admin') {
        http_response_code(403);
        exit('No tenés permisos para acceder a esta sección');
    }
}

Después podemos utilizar:

<?php

session_start();

require 'auth.php';

requireAdmin();

?>

Roles en una aplicación real

Para proyectos pequeños, guardar el rol directamente en la tabla usuarios puede ser suficiente.

Sin embargo, cuando el sistema crece suele ser mejor separar la información.

Por ejemplo:

CREATE TABLE roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(50) NOT NULL UNIQUE
);
ALTER TABLE usuarios
ADD rol_id INT,
ADD FOREIGN KEY (rol_id)
    REFERENCES roles(id);

¿Qué son los permisos?

Un rol es una agrupación de capacidades.

Por ejemplo, el rol admin podría tener estos permisos:

  • ver usuarios;
  • crear usuarios;
  • editar usuarios;
  • eliminar usuarios.

Mientras que el rol usuario podría tener solamente:

  • ver su perfil;
  • editar su perfil.

Modelo con roles y permisos

Una estructura más flexible podría utilizar:

  • usuarios;
  • roles;
  • permisos;
  • rol_permiso.

Esto permite asignar muchos permisos a cada rol.

Tabla de permisos

CREATE TABLE permisos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL UNIQUE
);

Tabla intermedia rol_permiso

CREATE TABLE rol_permiso (
    rol_id INT NOT NULL,
    permiso_id INT NOT NULL,

    PRIMARY KEY (rol_id, permiso_id),

    FOREIGN KEY (rol_id)
        REFERENCES roles(id),

    FOREIGN KEY (permiso_id)
        REFERENCES permisos(id)
);

Consultar los permisos de un usuario

SELECT p.nombre
FROM usuarios u
INNER JOIN roles r
    ON u.rol_id = r.id
INNER JOIN rol_permiso rp
    ON r.id = rp.rol_id
INNER JOIN permisos p
    ON rp.permiso_id = p.id
WHERE u.id = :usuario_id;

Función tienePermiso()

Una implementación simple podría ser:

function tienePermiso(PDO $pdo, int $usuarioId, string $permiso): bool
{
    $sql = 'SELECT COUNT(*)
            FROM usuarios u
            INNER JOIN roles r
                ON u.rol_id = r.id
            INNER JOIN rol_permiso rp
                ON r.id = rp.rol_id
            INNER JOIN permisos p
                ON rp.permiso_id = p.id
            WHERE u.id = :usuario_id
            AND p.nombre = :permiso';

    $stmt = $pdo->prepare($sql);

    $stmt->execute([
        'usuario_id' => $usuarioId,
        'permiso' => $permiso
    ]);

    return (bool) $stmt->fetchColumn();
}

Usar permisos para proteger acciones

if (!tienePermiso(
    $pdo,
    $_SESSION['usuario_id'],
    'usuarios.eliminar'
)) {
    http_response_code(403);
    exit('Acceso denegado');
}

Controlar acciones, no solo páginas

Los permisos deben aplicarse también sobre operaciones específicas.

Por ejemplo, aunque un usuario pueda acceder a una lista, eso no significa necesariamente que pueda:

  • crear registros;
  • editarlos;
  • eliminarlos;
  • exportarlos.

HTTP 401 y 403

Son dos códigos que suelen confundirse.

  • 401 Unauthorized: normalmente indica que falta autenticación válida.
  • 403 Forbidden: el usuario está identificado, pero no tiene permiso para realizar la acción.

No confiar en datos enviados por el cliente

Nunca debemos decidir permisos basándonos en un campo enviado desde un formulario.

Esto sería inseguro:

<input type="hidden" name="rol" value="admin">

El usuario puede modificar fácilmente ese valor.

El rol y los permisos deben obtenerse desde una fuente confiable, como la sesión y la base de datos.

Actualizar permisos durante una sesión

Si guardamos el rol únicamente en $_SESSION y luego un administrador cambia ese rol en la base de datos, la sesión podría conservar temporalmente el valor anterior.

En sistemas donde los permisos cambian con frecuencia puede ser conveniente consultar los permisos desde la base de datos o implementar una estrategia para actualizar la sesión.

Ejemplo de estructura de archivos

proyecto/
│
├── config/
│   └── conexion.php
│
├── auth/
│   └── auth.php
│
├── admin/
│   ├── index.php
│   └── usuarios.php
│
├── usuario/
│   └── perfil.php
│
├── login.php
├── logout.php
└── index.php

Esto ayuda a separar responsabilidades y mantener el proyecto ordenado.

Errores comunes

  • confundir autenticación con autorización;
  • proteger solamente los botones y no las acciones del servidor;
  • repetir controles de permisos por todo el proyecto;
  • confiar en roles enviados mediante formularios o parámetros;
  • dar permisos excesivos a usuarios comunes;
  • guardar demasiada información sensible dentro de la sesión;
  • no devolver una respuesta adecuada cuando falta autorización.

Ejercicio práctico

Extendé el sistema de login de la guía anterior para tener dos roles:

  • admin;
  • usuario.

Después implementá:

  1. una página accesible por cualquier usuario autenticado;
  2. una página disponible solamente para administradores;
  3. un menú que muestre opciones diferentes según el rol;
  4. protección en el servidor para las operaciones administrativas;
  5. una respuesta HTTP 403 cuando un usuario no tenga permisos.

¿Qué sigue?

En la próxima guía podemos avanzar con un tema fundamental para cualquier aplicación web: seguridad en PHP.

Veremos:

  • inyección SQL;
  • XSS;
  • CSRF;
  • validación y sanitización;
  • protección de formularios;
  • manejo seguro de contraseñas y sesiones.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Descubre más desde Club Programador

Suscríbete y recibe las últimas entradas en tu correo electrónico.

Deja un comentario

Descubre más desde Club Programador

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo