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:
- PHP 8.0 o superior
- Composer instalado
- Un servidor web (Apache/Nginx)
- Un editor de código
- 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.
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()]);
}
}
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>
5. Probando el sistema
Para probar nuestro sistema:
- Coloca los archivos en tu servidor web.
- Ajusta
$urlServidor
encliente/index.php
según tu configuración. - Accede a
http://localhost/jwt-auth/cliente/index.php
- Usa las credenciales:
- Usuario: demo
- Contraseña: password
6. Manejo de errores
El sistema maneja varios tipos de errores:
SignatureInvalidException:
- Ocurre cuando el token ha sido manipulado.
- Indica un posible intento de alteración.
BeforeValidException:
- El token se está usando antes de su tiempo de validez.
- Puede indicar problemas de sincronización de relojes.
ExpiredException:
- El token ha expirado.
- Requiere que el usuario inicie sesión nuevamente.
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