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:

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  1. Header (cabecera): contiene el tipo de token y el algoritmo de cifrado.

  2. Payload (cuerpo): contiene los datos del usuario (como su ID, rol, etc.) y metadatos (fecha de emisión, expiración).

  3. 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?

  1. El usuario envía sus credenciales (email/contraseña) al endpoint de login.

  2. El servidor valida las credenciales y, si son correctas, genera un JWT firmado con una clave secreta.

  3. El servidor devuelve el JWT al cliente (por ejemplo, en la respuesta JSON).

  4. El cliente guarda el token (normalmente en localStorage o en una cookie).

  5. En cada petición posterior, el cliente envía el token en el header Authorization: Bearer <token>.

  6. El servidor verifica la firma del token, comprueba que no haya expirado y extrae los datos del usuario.

  7. 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:

  1. Laravel Sanctum (recomendado): es el paquete oficial que ofrece autenticación con tokens (similares a JWT) y es muy sencillo de configurar.

  2. 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

bash
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Paso 2: Configurar el modelo User

En app/Models/User.php, asegúrate de usar el trait HasApiTokens:

php
<?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

bash
php artisan make:controller Api/AuthController

En app/Http/Controllers/Api/AuthController.php:

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
<?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:

bash
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:

json
{
    "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:

bash
curl -X POST http://localhost:8000/api/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "juan@example.com",
    "password": "password123"
  }'

Respuesta:

json
{
    "success": true,
    "message": "Inicio de sesión exitoso",
    "data": {
        "user": { ... },
        "access_token": "2|...",
        "token_type": "Bearer"
    }
}

Acceder a ruta protegida (ejemplo: obtener usuario autenticado):

bash
curl -X GET http://localhost:8000/api/user \
  -H "Authorization: Bearer 2|abcdefghijklmnop..."

Cerrar sesión:

bash
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

bash
composer require tymon/jwt-auth

Paso 2: Publicar la configuración

bash
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Paso 3: Generar la clave secreta

bash
php artisan jwt:secret

Esto creará una clave en tu archivo .env (JWT_SECRET=...).

Paso 4: Configurar el modelo User

En app/Models/User.php:

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

bash
php artisan make:controller Api/JWTAuthController

En app/Http/Controllers/Api/JWTAuthController.php:

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:

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:

php
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

Paso 8: Probar

Login:

bash
curl -X POST http://localhost:8000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"juan@example.com","password":"password123"}'

Usar token:

bash
curl -X GET http://localhost:8000/api/user \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."

🆚 Sanctum vs JWT-Auth: ¿Cuál elegir?

CaracterísticaSanctumJWT-Auth
MantenimientoOficial de LaravelTerceros (activo)
ComplejidadMuy sencilloModerada
TokensTokens tipo "Bearer" (no JWT puro)JWT estándar (RFC)
ExpiraciónConfigurable (tiempo de vida)Configurable (TTL)
Refresh tokenNo nativo (puedes implementarlo)Sí, con refresh()
Uso con SPAExcelente (cookies)Necesita almacenar token en cliente
EscalabilidadBuenaMuy 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

  1. Almacenamiento seguro: guarda el token en localStorage o sessionStorage (para SPA) o en cookies HttpOnly (más seguro).

  2. Tiempo de expiración corto: entre 15 minutos y 1 hora para el access token. Usa refresh tokens para sesiones largas.

  3. Usa HTTPS siempre en producción para evitar que el token sea interceptado.

  4. No incluyas información sensible en el payload (como contraseñas).

  5. Revoca tokens cuando el usuario cierre sesión o cambie su contraseña.

  6. Valida el token en cada petición (los middlewares de Laravel lo hacen automáticamente).

  7. 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:

  1. Instalar y configurar Sanctum (ya viene en Laravel 12).

  2. Crear el controlador AuthController con métodos register, login, logout, user.

  3. Definir rutas públicas y protegidas con auth:sanctum.

  4. 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

Entradas más populares de este blog

crear controladores separados para API y Web

Laravel tanto para web como para API al mismo tiempo.

Creación de una API RESTful con Laravel