89 lines
2.6 KiB
PHP
89 lines
2.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Middleware de autenticación JWT HS256.
|
|
*
|
|
* Valida el header: Authorization: Bearer <token>
|
|
* 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 (!str_starts_with($header, 'Bearer ')) {
|
|
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): never
|
|
{
|
|
http_response_code($code);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode(['error' => $message], JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
}
|