* El secreto se toma de la variable de entorno JWT_SECRET. */ class Auth { /** * Verifica el Bearer token. Termina con 401 si es inválido. * Devuelve el payload decodificado si es válido. */ public static function requireBearer(): array { $header = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; if (strpos($header, 'Bearer ') !== 0) { self::abort(401, 'Token de autorización requerido.'); } $token = substr($header, 7); return self::verifyJwt($token); } // ─── JWT HS256 ──────────────────────────────────────────────────────────── private static function verifyJwt(string $token): array { $parts = explode('.', $token); if (count($parts) !== 3) { self::abort(401, 'Token malformado.'); } [$b64Header, $b64Payload, $b64Sig] = $parts; // Verificar firma $secret = env('JWT_SECRET', ''); if ($secret === '') { self::abort(500, 'JWT_SECRET no configurado.'); } $data = "$b64Header.$b64Payload"; $expected = self::base64UrlEncode( hash_hmac('sha256', $data, $secret, true) ); // Comparación segura contra timing attacks if (!hash_equals($expected, $b64Sig)) { self::abort(401, 'Firma JWT inválida.'); } // Decodificar payload $payload = json_decode(self::base64UrlDecode($b64Payload), true); if (!is_array($payload)) { self::abort(401, 'Payload JWT inválido.'); } // Validar expiración si existe if (isset($payload['exp']) && $payload['exp'] < time()) { self::abort(401, 'Token expirado.'); } return $payload; } private static function base64UrlEncode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } private static function base64UrlDecode(string $data): string { return base64_decode(strtr($data, '-_', '+/')); } private static function abort(int $code, string $message): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['error' => $message], JSON_UNESCAPED_UNICODE); exit; } }