Construyendo una API segura de Laravel con autenticación JWT
La autenticación JWT (JSON Web Token) en Laravel proporciona un mecanismo criptográficamente firmado y sin estado para verificar consumidores de API sin almacenamiento de sesión en el servidor. Un JWT codifica un payload — típicamente identidad del usuario y claims — en una cadena compacta y segura para URL, firmada con una clave secreta o RSA, lo que permite que cualquier servicio que tenga la clave de verificación valide el token de forma independiente.
Esta guía cubre la implementación completa de autenticación JWT en una API de Laravel usando el paquete `tymon/jwt-auth`, incluyendo configuración, configuración del modelo, lógica del controlador, protección de rutas, estrategia de actualización de tokens y refuerzo para producción. Cada paso incluye contexto técnico que va más allá de los tutoriales superficiales.
Requisitos previos y supuestos del entorno
Antes de comenzar, confirme lo siguiente:
- PHP 8.1 o superior (PHP 8.2 recomendado para Laravel 11)
- Laravel 10 u 11 instalado mediante Composer
- Composer 2.x
- MySQL 8.0, PostgreSQL 15 o cualquier base de datos compatible con PDO
- Un archivo `.env` configurado con credenciales `DB_*` válidas
- Familiaridad básica con el contenedor de servicios, middleware y Eloquent ORM de Laravel
Si está desplegando esta API en un entorno de producción, un plan de Hosting VPS con acceso root completo le brinda el control necesario para configurar PHP-FPM, gestionar variables de entorno de forma segura y establecer permisos de archivos correctamente — todo ello crítico para la gestión del secreto JWT.
Paso 1: Crear un nuevo proyecto Laravel
“`bash
composer create-project laravel/laravel laravel-jwt-api
cd laravel-jwt-api
“`
Verifique la instalación:
“`bash
php artisan –version
“`
Establezca su clave de aplicación si no se generó automáticamente:
“`bash
php artisan key:generate
“`
El `APP_KEY` en `.env` es independiente del secreto JWT. Ambos son necesarios y sirven para diferentes propósitos criptográficos — `APP_KEY` protege las cookies cifradas y los datos de sesión, mientras que `JWT_SECRET` firma los tokens.
Paso 2: Instalar el paquete de autenticación JWT
El paquete `tymon/jwt-auth` es el estándar de facto para JWT en Laravel. Instálelo:
“`bash
composer require tymon/jwt-auth
“`
Publique la configuración del paquete:
“`bash
php artisan vendor:publish –provider="TymonJWTAuthProvidersLaravelServiceProvider"
“`
Esto crea `config/jwt.php`, que controla el TTL del token, el TTL de actualización, el algoritmo, el comportamiento de la lista negra y los claims requeridos. Revise este archivo cuidadosamente — los valores predeterminados son razonables para desarrollo, pero requieren ajuste para producción.
Parámetros de configuración clave en `config/jwt.php`:
| Parámetro | Predeterminado | Recomendación para producción |
|---|
| — | — | — |
|---|
| `ttl` | 60 minutos | 15–30 minutos |
|---|
| `refresh_ttl` | 20160 minutos (2 semanas) | 1440–10080 minutos |
|---|
| `algo` | `HS256` | `RS256` para sistemas distribuidos |
|---|
| `blacklist_enabled` | `true` | Debe ser `true` |
|---|
| `blacklist_grace_period` | 0 segundos | 10–30 segundos para solicitudes concurrentes |
|---|
| `required_claims` | `['iss','iat','exp','nbf','sub','jti']` | Conservar todos; agregar `aud` para APIs multi-tenant |
|---|
Paso 3: Generar la clave secreta JWT
“`bash
php artisan jwt:secret
“`
Esto agrega `JWT_SECRET` a su archivo `.env`. Este valor se usa como clave de firma HMAC-SHA256 para todos los tokens cuando se utiliza el algoritmo predeterminado `HS256`.
Notas críticas de seguridad:
- Nunca confirme `.env` en el control de versiones. Agréguelo a `.gitignore` de inmediato.
- En un pipeline de despliegue compartido, inyecte `JWT_SECRET` como variable de entorno a través de su sistema CI/CD en lugar de almacenarlo en un archivo.
- Si rota el secreto, todos los tokens existentes se invalidan inmediatamente. Planifique las ventanas de rotación en consecuencia y comuníquelas a los consumidores de la API.
- Para arquitecturas de microservicios donde múltiples servicios deben verificar tokens, cambie a `RS256`. Genere un par de claves RSA, almacene la clave privada en el servicio de autenticación y distribuya solo la clave pública a los servicios consumidores.
Paso 4: Configurar el guard de autenticación
Abra `config/auth.php` y actualice las secciones de defaults y guards:
“`php
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
“`
Esto indica al sistema de autenticación de Laravel que use el driver JWT al resolver el guard `api`. El middleware `auth:api` ahora delegará la validación de tokens a `tymon/jwt-auth` en lugar de Laravel Passport o el driver de token predeterminado.
No elimine el guard `web`. Muchos componentes internos de Laravel dependen de él, y eliminarlo causa fallos inesperados en comandos de consola y workers de cola que interactúan con el sistema de autenticación.
Paso 5: Crear el modelo User y la migración
Si el modelo `User` predeterminado y la migración ya existen (como ocurre en una instalación nueva de Laravel), puede modificarlos directamente. Si comienza desde cero:
“`bash
php artisan make:model User -m
“`
Abra el archivo de migración en `database/migrations/` y defina el esquema:
“`php
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
“`
Ejecute la migración:
“`bash
php artisan migrate
“`
Nota para producción: Siempre ejecute las migraciones con el indicador `–force` en entornos de producción donde `APP_ENV=production`, ya que Laravel solicitará confirmación de lo contrario:
“`bash
php artisan migrate –force
“`
Paso 6: Implementar la interfaz JWTSubject en el modelo User
El paquete `tymon/jwt-auth` requiere que su modelo autenticable implemente el contrato `JWTSubject`. Abra `app/Models/User.php`:
“`php
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateFoundationAuthUser as Authenticatable;
use IlluminateNotificationsNotifiable;
use TymonJWTAuthContractsJWTSubject;
class User extends Authenticatable implements JWTSubject
{
use HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
/**
- Get the identifier that will be stored in the JWT subject claim.
*/
public function getJWTIdentifier(): mixed
{
return $this->getKey();
}
/**
- Return a key-value array of arbitrary claims to add to the JWT payload.
*/
public function getJWTCustomClaims(): array
{
return [];
}
}
“`
Sobre los claims personalizados: El método `getJWTCustomClaims()` es donde se incorporan datos específicos de la aplicación directamente en el payload del token. Los casos de uso comunes incluyen incorporar `role`, `tenant_id` o `permissions` para que los servicios posteriores puedan tomar decisiones de autorización sin consultar la base de datos. Sea deliberado sobre lo que incorpora — cada claim aumenta el tamaño del token y es legible por cualquiera que decodifique el payload en base64. Nunca incorpore datos sensibles como contraseñas o PII.
Paso 7: Construir el controlador de autenticación
“`bash
php artisan make:controller AuthController
“`
Complete `app/Http/Controllers/AuthController.php` con la lógica de autenticación completa:
“`php
<?php
namespace AppHttpControllers;
use AppModelsUser;
use IlluminateHttpJsonResponse;
use IlluminateHttpRequest;
use IlluminateSupportFacadesAuth;
use IlluminateSupportFacadesHash;
use IlluminateValidationValidationException;
use TymonJWTAuthExceptionsJWTException;
use TymonJWTAuthFacadesJWTAuth;
class AuthController extends Controller
{
/**
- Register a new user and return a JWT.
*/
public function register(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
$token = JWTAuth::fromUser($user);
return response()->json([
'token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
], 201);
}
/**
- Authenticate a user and return a JWT.
*/
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
]);
if (!$token = Auth::guard('api')->attempt($credentials)) {
return response()->json(['error' => 'Invalid credentials'], 401);
}
return $this->respondWithToken($token);
}
/**
- Invalidate the current token (logout).
*/
public function logout(): JsonResponse
{
try {
Auth::guard('api')->logout();
} catch (JWTException $e) {
return response()->json(['error' => 'Failed to invalidate token'], 500);
}
return response()->json(['message' => 'Successfully logged out']);
}
/**
- Refresh the current token.
*/
public function refresh(): JsonResponse
{
try {
$token = Auth::guard('api')->refresh();
} catch (JWTException $e) {
return response()->json(['error' => 'Token cannot be refreshed'], 401);
}
return $this->respondWithToken($token);
}
/**
- Return the authenticated user's profile.
*/
public function me(): JsonResponse
{
return response()->json(Auth::guard('api')->user());
}
/**
- Format the token response consistently.
*/
protected function respondWithToken(string $token): JsonResponse
{
return response()->json([
'token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
]);
}
}
“`
Por qué es importante especificar el guard explícitamente: Llamar a `Auth::attempt()` sin especificar el guard recurre al guard predeterminado. Si ha cambiado el predeterminado a `api`, esto funciona — pero es frágil. Siempre llame a `Auth::guard('api')->attempt()` explícitamente para evitar errores sutiles cuando el guard predeterminado cambie durante la refactorización.
Paso 8: Definir las rutas de la API
Abra `routes/api.php` y defina las rutas de autenticación y las rutas protegidas:
“`php
<?php
use AppHttpControllersAuthController;
use IlluminateSupportFacadesRoute;
// Public authentication routes
Route::prefix('auth')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
});
// Protected routes — require a valid JWT
Route::middleware('auth:api')->prefix('auth')->group(function () {
Route::post('logout', [AuthController::class, 'logout']);
Route::post('refresh', [AuthController::class, 'refresh']);
Route::get('me', [AuthController::class, 'me']);
});
// Example protected resource routes
Route::middleware('auth:api')->group(function () {
Route::apiResource('posts', AppHttpControllersPostController::class);
});
“`
Estrategia de prefijo de rutas: Agrupar los endpoints de autenticación bajo `/api/auth/` es una convención ampliamente adoptada que hace la documentación de la API más limpia y simplifica las reglas de limitación de velocidad — puede aplicar una limitación más estricta a `/api/auth/login` de forma independiente de los endpoints de recursos.
Paso 9: Proteger rutas y gestionar fallos de middleware
El middleware `auth:api` de Laravel devolverá una respuesta `401 Unauthenticated` cuando un token esté ausente, haya expirado o sea inválido. Para devolver una respuesta JSON consistente en lugar de una redirección HTML, sobreescriba el método `unauthenticated` en `app/Exceptions/Handler.php`:
“`php
use IlluminateAuthAuthenticationException;
use IlluminateHttpRequest;
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson() || $request->is('api/*')) {
return response()->json(['error' => 'Unauthenticated'], 401);
}
return redirect()->guest(route('login'));
}
“`
Sin esta sobreescritura, los clientes de la API reciben una redirección HTML 302 a una página de inicio de sesión que no existe en una aplicación puramente API — una fuente común de confusión durante las pruebas de integración.
Paso 10: Estrategia de actualización de tokens y lista negra
La naturaleza sin estado de JWT crea una tensión: los tokens son autónomos y válidos hasta su expiración, pero necesita una forma de revocarlos al cerrar sesión. El paquete `tymon/jwt-auth` resuelve esto con una lista negra de tokens respaldada por el driver de caché de Laravel.
Cómo funciona la lista negra:
- Al cerrar sesión, el claim `jti` (ID JWT) del token se almacena en la caché con un TTL que coincide con el tiempo de vida restante del token.
- En cada solicitud autenticada, el middleware verifica la lista negra antes de aceptar el token.
- La configuración `blacklist_grace_period` permite una breve ventana donde un token que está siendo actualizado aún puede usarse, evitando condiciones de carrera en clientes que realizan solicitudes concurrentes.
Asegúrese de que su driver de caché lo soporte. El driver predeterminado `file` funciona para despliegues en un solo servidor. Para APIs escaladas horizontalmente que se ejecutan en múltiples nodos — común cuando se usan Servidores Dedicados en una configuración con balanceo de carga — cambie a Redis o Memcached:
“`env
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
“`
Flujo de actualización de tokens:
“`
Client API Server
| — POST /api/auth/refresh ——> |
|---|
| Authorization: Bearer <old> |
|---|
| — Validate old token |
|---|
| — Blacklist old token's jti |
|---|
| — Issue new token |
|---|
| <– 200 { token: <new> } ——– |
|---|
“`
El token antiguo se incluye inmediatamente en la lista negra al actualizarse. Los clientes deben almacenar el nuevo token y descartar el antiguo. Implemente esto como un interceptor de Axios o equivalente en su frontend para gestionar la actualización de tokens de forma transparente.
Paso 11: Probar la API
Use `curl` o Postman para verificar cada endpoint.
Registrar un usuario:
“`bash
curl -X POST https://your-domain.com/api/auth/register
-H "Content-Type: application/json"
-d '{
"name": "Jane Smith",
"email": "jane@example.com",
"password": "SecurePass123!",
"password_confirmation": "SecurePass123!"
}'
“`
Respuesta esperada (`201 Created`):
“`json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…",
"token_type": "bearer",
"expires_in": 3600
}
“`
Iniciar sesión:
“`bash
curl -X POST https://your-domain.com/api/auth/login
-H "Content-Type: application/json"
-d '{"email": "jane@example.com", "password": "SecurePass123!"}'
“`
Acceder a una ruta protegida:
“`bash
curl -X GET https://your-domain.com/api/auth/me
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…"
“`
Actualizar el token:
“`bash
curl -X POST https://your-domain.com/api/auth/refresh
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…"
“`
Cerrar sesión:
“`bash
curl -X POST https://your-domain.com/api/auth/logout
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…"
“`
JWT vs. Sesión vs. Laravel Sanctum vs. Passport
Entender cuándo JWT es la elección correcta requiere compararlo con las alternativas disponibles en el ecosistema Laravel.
| Criterio | JWT (`tymon/jwt-auth`) | Laravel Sanctum | Laravel Passport | Basado en sesión |
|---|
| — | — | — | — | — |
|---|
| **Estado** | Sin estado | Sin estado (tokens SPA) / Con estado (cookies) | Con estado (tokens en BD) | Con estado |
|---|
| **Almacenamiento de tokens** | Del lado del cliente | Del lado del cliente o cookie | Base de datos | Sesión del lado del servidor |
|---|
| **Revocación** | Lista negra (caché) | Inmediata (eliminación en BD) | Inmediata (eliminación en BD) | Inmediata |
|---|
| **Escalabilidad** | Excelente (sin BD por solicitud) | Buena | Moderada (consulta a BD por solicitud) | Deficiente (se necesita sincronización de sesión) |
|---|
| **Soporte OAuth2** | No | No | Sí (servidor OAuth2 completo) | No |
|---|
| **Complejidad** | Media | Baja | Alta | Baja |
|---|
| **Ideal para** | APIs sin estado, microservicios | SPAs, aplicaciones móviles | Clientes OAuth de terceros | Aplicaciones web tradicionales |
|---|
| **Introspección de tokens** | Decodificar payload del lado del cliente | Requiere llamada a la API | Requiere llamada a la API | N/A |
|---|
Cuándo elegir JWT sobre Sanctum: Si su API es consumida por clientes de terceros, aplicaciones móviles o microservicios que no pueden compartir una cookie de sesión o conexión de base de datos con su aplicación Laravel, la naturaleza autónoma de JWT es una ventaja significativa. Si está construyendo una SPA de primera parte en el mismo dominio, Sanctum con autenticación basada en cookies es más simple y evita por completo las preocupaciones de seguridad en el almacenamiento de tokens.
Refuerzo de seguridad para producción
Una implementación JWT funcional no es segura por defecto. Aplique estas medidas de refuerzo antes de salir a producción:
1. Aplicar HTTPS de forma incondicional
Los tokens JWT transmitidos por HTTP son interceptables de forma trivial. Aplique TLS a nivel del servidor web y redirija todo el tráfico HTTP. Combínelo con un Certificado SSL para garantizar el transporte cifrado en cada solicitud de la API.
2. Establecer TTLs de tokens agresivos
Los tokens de acceso de corta duración (15–30 minutos) combinados con tokens de actualización de mayor duración (7–14 días) limitan el impacto de un token robado. Actualice `config/jwt.php`:
“`php
'ttl' => 15,
'refresh_ttl' => 10080,
“`
3. Aplicar limitación de velocidad a los endpoints de autenticación
El middleware throttle integrado de Laravel previene ataques de fuerza bruta:
“`php
Route::middleware(['throttle:10,1'])->group(function () {
Route::post('auth/login', [AuthController::class, 'login']);
Route::post('auth/register', [AuthController::class, 'register']);
});
“`
Esto limita cada IP a 10 solicitudes por minuto en los endpoints de autenticación.
4. Validar el claim `aud` para APIs multi-tenant
Si su API sirve a múltiples aplicaciones cliente, incorpore y valide un claim `audience` para evitar la reutilización de tokens entre servicios:
“`php
// In getJWTCustomClaims()
return [
'aud' => config('app.jwt_audience'),
];
“`
5. Proteger el JWT_SECRET a nivel del sistema operativo
Establezca permisos de archivo restrictivos en `.env`:
“`bash
chmod 640 .env
chown www-data:www-data .env
“`
En un VPS con cPanel correctamente configurado, puede gestionar la propiedad y los permisos de archivos a través del Administrador de Archivos o SSH sin riesgo de exponer secretos a otros usuarios del sistema.
6. Registrar eventos de autenticación
Integre el sistema de eventos de Laravel para registrar intentos de inicio de sesión fallidos, actualizaciones de tokens y cierres de sesión en un servicio de registro centralizado. Esto es esencial para la detección de anomalías.
Agregar control de acceso basado en roles
Los claims personalizados facilitan la incorporación de roles directamente en el token:
“`php
// In User model
public function getJWTCustomClaims(): array
{
return [
'role' => $this->role, // e.g., 'admin', 'editor', 'viewer'
];
}
“`
Cree un middleware para aplicar los requisitos de roles:
“`php
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateHttpRequest;
use TymonJWTAuthFacadesJWTAuth;
class RoleMiddleware
{
public function handle(Request $request, Closure $next, string $role): mixed
{
$payload = JWTAuth::parseToken()->getPayload();
if ($payload->get('role') !== $role) {
return response()->json(['error' => 'Forbidden'], 403);
}
return $next($request);
}
}
“`
Regístrelo en `app/Http/Kernel.php` (Laravel 10) o `bootstrap/app.php` (Laravel 11) y aplíquelo a las rutas:
“`php
Route::middleware(['auth:api', 'role:admin'])->group(function () {
Route::apiResource('admin/users', AdminUserController::class);
});
“`
Advertencia: Los datos de roles incorporados en el token son tan actuales como el propio token. Si el rol de un usuario cambia, el token antiguo continúa otorgando el rol antiguo hasta que expire o se actualice. Para cambios de roles de alta seguridad (p. ej., revocar acceso de administrador de inmediato), combine la lista negra de tokens con un TTL corto o realice una verificación de roles en la base de datos en el middleware junto con la verificación del claim.
Consideraciones de despliegue para APIs Laravel JWT
Al pasar del desarrollo local a un servidor de producción, varios factores específicos del entorno afectan el comportamiento de JWT:
- Consistencia de zona horaria: Los claims `iat`, `nbf` y `exp` de JWT son marcas de tiempo Unix. Asegúrese de que la zona horaria de su servidor esté configurada en UTC (`date.timezone = UTC` en `php.ini`) para evitar el rechazo de tokens por desfase de reloj.
- OPcache: Habilite PHP OPcache para reducir la sobrecarga de cargar los archivos de la biblioteca JWT en cada solicitud. Esto es especialmente impactante en APIs de alto tráfico.
- Workers de cola para limpieza de tokens: Si implementa trabajos personalizados de limpieza de lista negra de tokens, asegúrese de que su worker de cola se ejecute como un proceso supervisado (Supervisor o systemd).
- Gestión de variables de entorno: En los Paneles de Control VPS, use el gestor de variables de entorno del panel en lugar de editar `.env` directamente en producción, para evitar sobreescrituras accidentales durante los despliegues.
Lista de verificación antes de salir a producción
Use esta lista de verificación para confirmar que su implementación está lista para producción:
- `JWT_SECRET` tiene al menos 32 caracteres, generado aleatoriamente y no confirmado en el control de versiones
- `blacklist_enabled` está configurado como `true` en `config/jwt.php`
- El TTL del token es de 30 minutos o menos para los tokens de acceso
- El TTL de actualización está configurado con un valor apropiado para su política de sesión
- Todos los endpoints de la API se sirven exclusivamente a través de HTTPS
- La limitación de velocidad se aplica a los endpoints `/login` y `/register`
- El manejador de excepciones `unauthenticated` devuelve JSON, no una redirección HTML
- Los claims personalizados no contienen contraseñas, secretos ni PII sensible
- El driver de caché es Redis o Memcached (no `file`) en despliegues multi-servidor
- Los eventos de autenticación se registran y monitorean
- Los cambios de roles que requieren efecto inmediato se gestionan mediante lista negra, no solo por expiración de claims
- Los permisos del archivo `.env` están restringidos al usuario del servidor web
Preguntas frecuentes
¿Cuál es la diferencia entre `JWT_SECRET` y `APP_KEY` en Laravel?
`APP_KEY` es utilizado por el servicio de cifrado de Laravel para cifrar cookies, datos de sesión y valores pasados a través de `Crypt::encrypt()`. `JWT_SECRET` es utilizado exclusivamente por `tymon/jwt-auth` para firmar y verificar JSON Web Tokens. Son criptográficamente independientes y sirven para propósitos completamente diferentes. Ambos deben mantenerse en secreto.
¿Por qué mi token JWT sigue devolviendo 401 aunque no haya expirado?
Las causas más comunes son: el token ha sido incluido en la lista negra (p. ej., después de un cierre de sesión o actualización), el `JWT_SECRET` fue rotado después de que se emitió el token, el driver de caché que almacena la lista negra no está disponible, o hay un desfase de reloj entre el servidor emisor y el servidor validador que supera la configuración `leeway` en `config/jwt.php`. Verifique cada uno de estos en orden.
¿Puedo usar la autenticación JWT con colas de Laravel o comandos de consola?
JWT está diseñado para la autenticación de solicitudes HTTP. Dentro de trabajos de cola o comandos Artisan, no hay contexto de solicitud HTTP, por lo que no puede resolver un usuario desde un token mediante middleware. En su lugar, pase la clave primaria del usuario como parámetro del trabajo y cargue el modelo directamente con `User::find($userId)`.
¿Cómo gestiono las solicitudes concurrentes durante la actualización de tokens sin obtener errores 401?
Establezca `blacklist_grace_period` en `config/jwt.php` con un valor entre 10 y 30 segundos. Durante esta ventana, un token que acaba de ser actualizado (y técnicamente incluido en la lista negra) seguirá siendo aceptado. Esto evita condiciones de carrera en clientes que envían múltiples solicitudes simultáneas mientras una actualización está en progreso.
¿Es `tymon/jwt-auth` compatible con Laravel 11?
A partir del ciclo de lanzamiento actual, `tymon/jwt-auth` versión `2.x` es compatible con Laravel 10 y 11 a través de la rama `dev-develop` o lanzamientos etiquetados que declaran compatibilidad. Siempre verifique las restricciones `composer.json` del paquete y el rastreador de problemas de GitHub antes de actualizar las versiones de Laravel en un proyecto que depende de este paquete. Considere fijar la versión del paquete en `composer.json` para evitar cambios disruptivos inesperados durante `composer update`.
