Saltar al contenido

JSON Web Tokens en PHP con php-jwt usando cliente real

Índice

Introducción

La gestión de la autenticación y sesiones en aplicaciones web modernas requiere soluciones robustas y seguras. JSON Web Tokens (JWT) se ha convertido en un estándar de facto para manejar este aspecto crucial del desarrollo web. En esta guía, aprenderemos a implementar un sistema de autenticación usando PHP-JWT, una librería mantenida por Firebase que facilita la creación y verificación de tokens JWT en aplicaciones PHP.

Esta implementación es especialmente útil cuando necesitamos: - Autenticación sin estado (stateless) - Comunicación segura entre servicios - APIs RESTful - Microservicios - Single Sign-On (SSO)

Requisitos previos

Para seguir este tutorial necesitarás:

  1. PHP 8.0 o superior
  2. Composer instalado
  3. Un servidor web (Apache/Nginx)
  4. Un editor de código
  5. Conocimientos básicos de PHP y JSON

Estructura del proyecto

Nuestro proyecto tendrá la siguiente estructura:

jwt-auth/
├── vendor/
├── cliente/
│   └── index.php
├── servidor/
│   ├── autenticacion.php
│   └── clave_secreta.php
└── composer.json

1. Configuración inicial

Primero, creamos nuestro proyecto e instalamos las dependencias necesarias:

mkdir jwt-auth
cd jwt-auth
composer require firebase/php-jwt
mkdir servidor cliente

La librería php-jwt se instalará en el directorio vendor/ junto con el autoloader de Composer.

2. Generación y manejo de la clave secreta

La clave secreta es crucial para la seguridad de nuestros tokens JWT. Crearemos un sistema que genere y mantenga una clave persistente en formato binario.

Creamos servidor/clave_secreta.php:

<?php
$archivoClaveSecreta = __DIR__ . '/clave_secreta.bin';

if (!file_exists($archivoClaveSecreta)) {
    // Generar una clave aleatoria de 256 bits
    $claveSecretaBinaria = random_bytes(32);
    
    // Guardar la clave binaria en un archivo
    file_put_contents($archivoClaveSecreta, $claveSecretaBinaria);
} else {
    // Leer la clave existente del archivo
    $claveSecretaBinaria = file_get_contents($archivoClaveSecreta);
}

// Convertir la clave a formato hexadecimal y devolverla
return bin2hex($claveSecretaBinaria);

Este script: - Verifica si existe un archivo de clave. - Si no existe, genera una clave aleatoria de 256 bits. - Convierte la clave a formato hexadecimal para su uso. - Mantiene la misma clave entre reinicios del servidor.

Archivo clave_secreta.bin generado

Archivo clave_secreta.bin generado

3. Implementación del servidor

El servidor manejará las solicitudes de autenticación y verificación. Creamos servidor/autenticacion.php:

<?php
require_once '../vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

header('Content-Type: application/json');

// Establecer zona horaria a Guatemala
date_default_timezone_set('America/Guatemala');

// Cargar la clave secreta desde un archivo separado
$claveHex = require_once 'clave_secreta.php';
$claveSecreta = hex2bin($claveHex);

// Función para generar un token JWT
function generarToken($usuario) {
    global $claveSecreta;
    $tiempoEmision = time();
    $tiempoExpiracion = $tiempoEmision + 3600; // Token válido por 1 hora

    $payload = [
        'iss' => 'http://ejemplo.org',
        'aud' => 'http://ejemplo.com',
        'iat' => $tiempoEmision,
        'nbf' => $tiempoEmision,
        'exp' => $tiempoExpiracion,
        'datos' => [
            'usuario' => $usuario,
        ]
    ];

    return JWT::encode($payload, $claveSecreta, 'HS256');
}

// Manejar solicitud de inicio de sesión
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $entrada = json_decode(file_get_contents('php://input'), true);
    $usuario = $entrada['usuario'] ?? '';
    $contrasena = $entrada['contrasena'] ?? '';

    // Autenticación simple (reemplazar con tu propia lógica)
    if ($usuario === 'demo' && $contrasena === 'password') {
        $token = generarToken($usuario);
        echo json_encode(['estado' => 'exito', 'token' => $token]);
    } else {
        echo json_encode(['estado' => 'error', 'mensaje' => 'Credenciales inválidas']);
    }
}

// Manejar verificación de token
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    $cabeceras = getallheaders();
    $jwt = $cabeceras['Authorization'] ?? '';

    if (empty($jwt)) {
        echo json_encode(['estado' => 'error', 'mensaje' => 'No se proporcionó token']);
        exit;
    }

    try {
        $decodificado = JWT::decode($jwt, new Key($claveSecreta, 'HS256'));
        echo json_encode([
            'estado' => 'exito',
            'mensaje' => 'Token válido',
            'datos' => (array) $decodificado
        ]);
    } catch (Firebase\JWT\SignatureInvalidException $e) {
        echo json_encode(['estado' => 'error', 'mensaje' => 'Firma inválida']);
    } catch (Firebase\JWT\BeforeValidException $e) {
        echo json_encode(['estado' => 'error', 'mensaje' => 'Token aún no válido']);
    } catch (Firebase\JWT\ExpiredException $e) {
        echo json_encode(['estado' => 'error', 'mensaje' => 'Token expirado']);
    } catch (Exception $e) {
        echo json_encode(['estado' => 'error', 'mensaje' => 'Ocurrió un error: ' . $e->getMessage()]);
    }
}
Respuesta JSON del servidor

Respuesta JSON del servidor

4. Implementación del cliente

El cliente proporcionará una interfaz web para probar el sistema. Creamos cliente/index.php:

<?php
// Establecer zona horaria a Guatemala
date_default_timezone_set('America/Guatemala');

$urlServidor = 'http://localhost/jwt-auth/servidor/autenticacion.php';

function enviarSolicitud($url, $metodo, $datos = null, $cabeceras = []) {
    $opciones = [
        'http' => [
            'method' => $metodo,
            'header' => implode("\r\n", $cabeceras),
            'content' => $datos ? json_encode($datos) : null,
        ],
    ];
    $contexto = stream_context_create($opciones);
    $resultado = file_get_contents($url, false, $contexto);
    return $resultado ? json_decode($resultado, true) : null;
}

$mensaje = '';
$token = '';
$datosToken = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $usuario = $_POST['usuario'] ?? '';
    $contrasena = $_POST['contrasena'] ?? '';

    $datosLogin = ['usuario' => $usuario, 'contrasena' => $contrasena];
    $respuesta = enviarSolicitud(
        $urlServidor, 
        'POST', 
        $datosLogin, 
        ['Content-Type: application/json']
    );

    if ($respuesta && $respuesta['estado'] === 'exito') {
        $token = $respuesta['token'];
        $mensaje = "Inicio de sesión exitoso. Token: " . $token;

        // Verificar el token
        $respuestaVerificacion = enviarSolicitud(
            $urlServidor, 
            'GET', 
            null, 
            [
                'Content-Type: application/json',
                'Authorization: ' . $token
            ]
        );

        if ($respuestaVerificacion && $respuestaVerificacion['estado'] === 'exito') {
            $datosToken = json_encode($respuestaVerificacion['datos'], JSON_PRETTY_PRINT);
        } else {
            $mensaje .= "<br>Falló la verificación del token: " . 
                ($respuestaVerificacion['mensaje'] ?? 'Error desconocido');
        }
    } else {
        $mensaje = "Falló el inicio de sesión: " . 
            ($respuesta['mensaje'] ?? 'Error desconocido');
    }
}
?>

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cliente de Inicio de Sesión JWT</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        form { margin-bottom: 20px; }
        input, button { margin: 5px 0; padding: 8px; width: 200px; }
        button { background-color: #4CAF50; color: white; border: none; cursor: pointer; }
        button:hover { background-color: #45a049; }
        pre { background-color: #f4f4f4; padding: 10px; white-space: pre-wrap; 
              word-wrap: break-word; border-radius: 4px; }
    </style>
</head>
<body>
    <h1>Cliente de Inicio de Sesión JWT</h1>
    <form method="post">
        <input type="text" name="usuario" placeholder="Usuario" required><br>
        <input type="password" name="contrasena" placeholder="Contraseña" required><br>
        <button type="submit">Iniciar Sesión</button>
    </form>

    <?php if ($mensaje): ?>
        <h2>Respuesta del Servidor:</h2>
        <p><?php echo $mensaje; ?></p>
    <?php endif; ?>

    <?php if ($datosToken): ?>
        <h2>Datos del Token:</h2>
        <pre><?php echo $datosToken; ?></pre>
    <?php endif; ?>
</body>
</html>
Interfaz del cliente web

Interfaz del cliente web

5. Probando el sistema

Para probar nuestro sistema:

  1. Coloca los archivos en tu servidor web.
  2. Ajusta $urlServidor en cliente/index.php según tu configuración.
  3. Accede a http://localhost/jwt-auth/cliente/index.php
  4. Usa las credenciales:
    • Usuario: demo
    • Contraseña: password
Login exitoso con token generado

Login exitoso con token generado

6. Manejo de errores

El sistema maneja varios tipos de errores:

  1. SignatureInvalidException:

    • Ocurre cuando el token ha sido manipulado.
    • Indica un posible intento de alteración.
  2. BeforeValidException:

    • El token se está usando antes de su tiempo de validez.
    • Puede indicar problemas de sincronización de relojes.
  3. ExpiredException:

    • El token ha expirado.
    • Requiere que el usuario inicie sesión nuevamente.
Error por token inválido

Error por token inválido

7. Consideraciones de seguridad

Protección de la clave secreta

  • Nunca versiones clave_secreta.bin
  • Usa permisos restrictivos (600).
  • Considera usar servicios de gestión de secretos en producción.

Configuración de tokens

  • Usa tiempos de expiración cortos.
  • Incluye claims relevantes (iss, aud, sub).
  • Implementa renovación de tokens.

Transmisión segura

  • Usa HTTPS en producción.
  • Implementa CORS adecuadamente.
  • Valida todas las entradas de usuario.

Conclusión

Hemos implementado un sistema de autenticación básico pero funcional usando PHP-JWT. Este sistema proporciona: - Autenticación sin estado. - Manejo seguro de claves secretas. - Validación de tokens. - Manejo de errores robusto.

Algunas mejoras posibles incluyen: - Sistema de renovación de tokens. - Lista negra de tokens revocados. - Integración con base de datos. - Implementación de refresh tokens.

Tarea

Identifica la ubicación correcta de los archivos y directorios. Pista: el script de la clave secreta y la clave misma no deben estar en el directorio público.

Referencias: - RFC 7519 - JSON Web Token (JWT) - Firebase PHP-JWT - OWASP JWT Cheat Sheet