Autenticación con JWT en Laravel 12 para principiantes
Autenticación con JWT en Laravel 12 para principiantes
Si has visto que muchas APIs usan JWT para autenticación y te preguntas qué es y cómo implementarlo, estás en el lugar correcto. Te explicaré todo desde cero, con un lenguaje sencillo y ejemplos prácticos.
📖 ¿Qué es JWT?
JWT significa JSON Web Token. Es un estándar (RFC 7519) que permite transmitir información de forma segura entre dos partes (por ejemplo, tu aplicación y tu API) en formato JSON.
Imagina que JWT es como una tarjeta de identidad digital que tu aplicación recibe después de iniciar sesión. Cada vez que quieras hacer una petición a la API, presentas esa tarjeta y el servidor sabe quién eres sin necesidad de preguntarte de nuevo tu usuario y contraseña.
¿Cómo es un JWT por dentro?
Un JWT tiene tres partes separadas por puntos:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cHeader (cabecera): contiene el tipo de token y el algoritmo de cifrado.
Payload (cuerpo): contiene los datos del usuario (como su ID, rol, etc.) y metadatos (fecha de emisión, expiración).
Signature (firma): se genera combinando el header y el payload con una clave secreta. Sirve para verificar que el token no ha sido alterado.
🔄 ¿Cómo funciona el flujo de autenticación con JWT?
El usuario envía sus credenciales (email/contraseña) al endpoint de login.
El servidor valida las credenciales y, si son correctas, genera un JWT firmado con una clave secreta.
El servidor devuelve el JWT al cliente (por ejemplo, en la respuesta JSON).
El cliente guarda el token (normalmente en localStorage o en una cookie).
En cada petición posterior, el cliente envía el token en el header
Authorization: Bearer <token>.El servidor verifica la firma del token, comprueba que no haya expirado y extrae los datos del usuario.
Si la verificación es exitosa, el servidor procesa la petición.
✅ Ventajas de JWT
Stateless: el servidor no necesita guardar sesiones en base de datos. Cada token contiene toda la información necesaria.
Escalable: al no depender de almacenamiento en servidor, es ideal para microservicios y APIs distribuidas.
Seguro: la firma garantiza que el token no haya sido manipulado.
Versátil: puede incluir datos adicionales (roles, permisos) y tener tiempo de expiración.
🛠️ Implementación en Laravel 12
Laravel 12 nos da dos opciones principales para JWT:
Laravel Sanctum (recomendado): es el paquete oficial que ofrece autenticación con tokens (similares a JWT) y es muy sencillo de configurar.
tymon/jwt-auth: paquete de terceros muy popular que implementa JWT puro.
Ambos funcionan bien, pero para principiantes recomiendo Sanctum porque está integrado en Laravel y su configuración es más simple. Además, sus tokens son seguros y también pueden tener expiración.
🔹 Opción 1: Autenticación con Laravel Sanctum (recomendada)
Laravel Sanctum ya viene instalado en Laravel 12. Solo necesitas publicar su configuración y migrar la tabla de tokens personales.
Paso 1: Instalar y configurar Sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migratePaso 2: Configurar el modelo User
En app/Models/User.php, asegúrate de usar el trait HasApiTokens:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
}Paso 3: Crear un controlador para autenticación
php artisan make:controller Api/AuthControllerEn app/Http/Controllers/Api/AuthController.php:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* Registro de nuevos usuarios.
*/
public function register(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
// Crear un token para el nuevo usuario
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'success' => true,
'message' => 'Usuario registrado exitosamente',
'data' => [
'user' => $user,
'access_token' => $token,
'token_type' => 'Bearer',
]
], 201);
}
/**
* Inicio de sesión.
*/
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (!Auth::attempt($credentials)) {
throw ValidationException::withMessages([
'email' => ['Las credenciales proporcionadas son incorrectas.'],
]);
}
$user = User::where('email', $request->email)->firstOrFail();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'success' => true,
'message' => 'Inicio de sesión exitoso',
'data' => [
'user' => $user,
'access_token' => $token,
'token_type' => 'Bearer',
]
]);
}
/**
* Cerrar sesión (revocar token).
*/
public function logout(Request $request)
{
// Eliminar el token actual del usuario
$request->user()->currentAccessToken()->delete();
return response()->json([
'success' => true,
'message' => 'Sesión cerrada correctamente'
]);
}
/**
* Obtener el usuario autenticado.
*/
public function user(Request $request)
{
return response()->json([
'success' => true,
'data' => $request->user()
]);
}
}Paso 4: Definir rutas para autenticación
En routes/api.php:
<?php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\PastelController;
use Illuminate\Support\Facades\Route;
// Rutas públicas (login, registro)
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Rutas protegidas por Sanctum
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', [AuthController::class, 'user']);
// Rutas de pasteles (ejemplo de CRUD)
Route::apiResource('pasteles', PastelController::class);
});Paso 5: Proteger rutas de la API
Ya lo hicimos en el paso anterior con el middleware auth:sanctum. Asegúrate de que en app/Http/Kernel.php (Laravel 11/12 usa bootstrap/app.php, pero no es necesario modificar nada; el middleware ya está registrado).
Paso 6: Probar la autenticación
Registro:
curl -X POST http://localhost:8000/api/register \
-H "Content-Type: application/json" \
-d '{
"name": "Juan Pérez",
"email": "juan@example.com",
"password": "password123",
"password_confirmation": "password123"
}'Respuesta esperada:
{
"success": true,
"message": "Usuario registrado exitosamente",
"data": {
"user": {
"id": 1,
"name": "Juan Pérez",
"email": "juan@example.com"
},
"access_token": "1|abcdefghijklmnop...",
"token_type": "Bearer"
}
}Login:
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-d '{
"email": "juan@example.com",
"password": "password123"
}'Respuesta:
{
"success": true,
"message": "Inicio de sesión exitoso",
"data": {
"user": { ... },
"access_token": "2|...",
"token_type": "Bearer"
}
}Acceder a ruta protegida (ejemplo: obtener usuario autenticado):
curl -X GET http://localhost:8000/api/user \
-H "Authorization: Bearer 2|abcdefghijklmnop..."Cerrar sesión:
curl -X POST http://localhost:8000/api/logout \
-H "Authorization: Bearer 2|abcdefghijklmnop..."🔹 Opción 2: JWT puro con tymon/jwt-auth
Si prefieres usar JWT estándar (con el formato exacto de tres partes), puedes usar el paquete tymon/jwt-auth. Es muy popular y ofrece más control.
Paso 1: Instalar el paquete
composer require tymon/jwt-authPaso 2: Publicar la configuración
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"Paso 3: Generar la clave secreta
php artisan jwt:secretEsto creará una clave en tu archivo .env (JWT_SECRET=...).
Paso 4: Configurar el modelo User
En app/Models/User.php:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
use Notifiable;
// ... tus fillable, hidden, etc.
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}Paso 5: Crear controlador de autenticación
php artisan make:controller Api/JWTAuthControllerEn app/Http/Controllers/Api/JWTAuthController.php:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
class JWTAuthController extends Controller
{
public function register(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
$token = JWTAuth::fromUser($user);
return response()->json([
'success' => true,
'message' => 'Usuario registrado',
'data' => [
'user' => $user,
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60 // segundos
]
], 201);
}
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
if (!$token = JWTAuth::attempt($credentials)) {
throw ValidationException::withMessages([
'email' => ['Las credenciales son incorrectas.'],
]);
}
return response()->json([
'success' => true,
'message' => 'Login exitoso',
'data' => [
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
]
]);
}
public function logout()
{
auth()->logout();
return response()->json([
'success' => true,
'message' => 'Sesión cerrada'
]);
}
public function user()
{
return response()->json([
'success' => true,
'data' => auth()->user()
]);
}
public function refresh()
{
try {
$newToken = auth()->refresh();
return response()->json([
'success' => true,
'data' => [
'access_token' => $newToken,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
]
]);
} catch (JWTException $e) {
return response()->json([
'success' => false,
'message' => 'No se pudo refrescar el token'
], 401);
}
}
}Paso 6: Configurar rutas
En routes/api.php:
use App\Http\Controllers\Api\JWTAuthController;
Route::post('/register', [JWTAuthController::class, 'register']);
Route::post('/login', [JWTAuthController::class, 'login']);
Route::middleware('auth:api')->group(function () {
Route::post('/logout', [JWTAuthController::class, 'logout']);
Route::get('/user', [JWTAuthController::class, 'user']);
Route::post('/refresh', [JWTAuthController::class, 'refresh']);
// Rutas protegidas adicionales
});Paso 7: Configurar el guard de autenticación
En config/auth.php, asegúrate de que el guard api use el driver jwt:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],Paso 8: Probar
Login:
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"juan@example.com","password":"password123"}'Usar token:
curl -X GET http://localhost:8000/api/user \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."🆚 Sanctum vs JWT-Auth: ¿Cuál elegir?
| Característica | Sanctum | JWT-Auth |
|---|---|---|
| Mantenimiento | Oficial de Laravel | Terceros (activo) |
| Complejidad | Muy sencillo | Moderada |
| Tokens | Tokens tipo "Bearer" (no JWT puro) | JWT estándar (RFC) |
| Expiración | Configurable (tiempo de vida) | Configurable (TTL) |
| Refresh token | No nativo (puedes implementarlo) | Sí, con refresh() |
| Uso con SPA | Excelente (cookies) | Necesita almacenar token en cliente |
| Escalabilidad | Buena | Muy buena |
Recomendación para principiantes: Sanctum es más fácil y suficiente para la mayoría de proyectos. Si necesitas cumplir con estándares JWT o necesitas refresh tokens, elige tymon/jwt-auth.
📌 Buenas prácticas con JWT
Almacenamiento seguro: guarda el token en
localStorageosessionStorage(para SPA) o en cookiesHttpOnly(más seguro).Tiempo de expiración corto: entre 15 minutos y 1 hora para el access token. Usa refresh tokens para sesiones largas.
Usa HTTPS siempre en producción para evitar que el token sea interceptado.
No incluyas información sensible en el payload (como contraseñas).
Revoca tokens cuando el usuario cierre sesión o cambie su contraseña.
Valida el token en cada petición (los middlewares de Laravel lo hacen automáticamente).
Maneja errores de token expirado o inválido con respuestas claras (código 401).
🧪 Ejemplo completo con Sanctum (resumen)
Aquí tienes el flujo completo con Sanctum, el más recomendado para empezar:
Instalar y configurar Sanctum (ya viene en Laravel 12).
Crear el controlador
AuthControllercon métodosregister,login,logout,user.Definir rutas públicas y protegidas con
auth:sanctum.Probar con cURL o Postman.
Con esto, tu API ya tiene autenticación segura y lista para ser consumida por un frontend (React, Vue, Angular) o una app móvil.
📝 Conclusión
JWT es una herramienta poderosa y estándar para autenticar APIs. Con Laravel 12 tienes dos caminos excelentes: Sanctum (sencillo y oficial) o JWT-Auth (más flexible y estándar). Como principiante, te recomiendo empezar con Sanctum, entender su funcionamiento, y luego, si lo necesitas, migrar a JWT-Auth para tener control total sobre el token.
¡Ya estás listo para implementar autenticación con JWT en tu API!
Comentarios
Publicar un comentario