JWT desde cero en PHP sin librerías

Implementación completa de JSON Web Tokens en PHP sin ninguna librería externa. Funciones jwt_encode y jwt_decode con algoritmo HS256, validación de expiración, emisor y tipo. Incluye demo de creación, validación y detección de tokens manipulados.
				<?php
/**
 * JWT desde cero en PHP sin librerías externas
 * Algoritmo HS256 (HMAC-SHA256)
 * Compatible PHP 8.1+
 */

// ?????????????????????????????????????????????????????????????
// Funciones auxiliares: base64url
// JWT usa base64url (sin padding, - en lugar de +, _ en lugar de /)
// ?????????????????????????????????????????????????????????????

function base64url_encode(string $data): string
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function base64url_decode(string $data): string|false
{
    // Restaurar padding y caracteres estándar antes de decodificar
    $padded = strtr($data, '-_', '+/');
    $padded .= str_repeat('=', (4 - strlen($padded) % 4) % 4);
    return base64_decode($padded);
}

// ?????????????????????????????????????????????????????????????
// Crear JWT
// Un JWT tiene 3 partes separadas por '.': header.payload.signature
// ?????????????????????????????????????????????????????????????

function jwt_encode(array $payload, string $secret): string
{
    // Header: tipo de token y algoritmo
    $header = ['typ' => 'JWT', 'alg' => 'HS256'];

    $headerB64  = base64url_encode(json_encode($header));
    $payloadB64 = base64url_encode(json_encode($payload));

    // La firma se calcula sobre header.payload
    $unsigned  = $headerB64 . '.' . $payloadB64;
    $signature = hash_hmac('sha256', $unsigned, $secret, true); // raw binary
    $sigB64    = base64url_encode($signature);

    return $unsigned . '.' . $sigB64;
}

// ?????????????????????????????????????????????????????????????
// Verificar y decodificar JWT
// ?????????????????????????????????????????????????????????????

function jwt_decode(string $token, string $secret): array
{
    $parts = explode('.', $token);
    if (count($parts) !== 3) {
        throw new InvalidArgumentException('Formato de token inválido');
    }

    [$headerB64, $payloadB64, $sigB64] = $parts;

    // 1. Recalcular la firma y comparar (timing-safe)
    $unsigned         = $headerB64 . '.' . $payloadB64;
    $expectedSig      = base64url_encode(hash_hmac('sha256', $unsigned, $secret, true));
    if (!hash_equals($expectedSig, $sigB64)) {
        throw new RuntimeException('Firma inválida — token manipulado o secret incorrecto');
    }

    // 2. Decodificar header y payload
    $header  = json_decode(base64url_decode($headerB64),  true);
    $payload = json_decode(base64url_decode($payloadB64), true);

    // 3. Validar tipo y algoritmo
    if (($header['typ'] ?? '') !== 'JWT' || ($header['alg'] ?? '') !== 'HS256') {
        throw new RuntimeException('Tipo o algoritmo de token no soportado');
    }

    // 4. Validar expiración (claim estándar 'exp')
    if (isset($payload['exp']) && $payload['exp'] < time()) {
        throw new RuntimeException('Token expirado');
    }

    return $payload;
}

// ?????????????????????????????????????????????????????????????
// Demo
// ?????????????????????????????????????????????????????????????

$secret = 'mi_secreto_super_seguro_32_chars!';

echo "=== JWT desde cero en PHP ===nn";

// Crear token de sesión con expiración en 1 hora
$payload = [
    'iss'  => 'miapp.com',            // emisor
    'sub'  => '42',                    // ID de usuario
    'name' => 'Ada Lovelace',
    'role' => 'admin',
    'iat'  => time(),                  // issued at
    'exp'  => time() + 3600,           // expira en 1 hora
];

$token = jwt_encode($payload, $secret);
echo "Token generado:n{$token}nn";

// Verificar token válido
try {
    $decoded = jwt_decode($token, $secret);
    echo "Token verificado correctamente.n";
    echo "Usuario: {$decoded['name']} (ID {$decoded['sub']}, rol: {$decoded['role']})nn";
} catch (Exception $e) {
    echo "Error: " . $e->getMessage() . "nn";
}

// Intentar con secret incorrecto
echo "--- Prueba con secret incorrecto ---n";
try {
    jwt_decode($token, 'secret_equivocado');
    echo "ERROR: debería haber falladon";
} catch (Exception $e) {
    echo "Detectado: " . $e->getMessage() . "nn";
}

// Token manipulado (cambiar un carácter del payload)
echo "--- Prueba con token manipulado ---n";
$parts       = explode('.', $token);
$parts[1]    = substr($parts[1], 0, -1) . 'X'; // alterar último char del payload
$tampered    = implode('.', $parts);
try {
    jwt_decode($tampered, $secret);
    echo "ERROR: debería haber falladon";
} catch (Exception $e) {
    echo "Detectado: " . $e->getMessage() . "nn";
}

// Token expirado
echo "--- Prueba con token expirado ---n";
$expiredPayload = ['sub' => '1', 'exp' => time() - 1]; // ya expiró
$expiredToken   = jwt_encode($expiredPayload, $secret);
try {
    jwt_decode($expiredToken, $secret);
    echo "ERROR: debería haber falladon";
} catch (Exception $e) {
    echo "Detectado: " . $e->getMessage() . "n";
}

			
Descargar adjuntos
COMPARTE ESTE TUTORIAL

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
TUTORIAL ANTERIOR