Tabla de contenido

Cuando construyes una API REST con Laravel, necesitas una forma de saber quién está haciendo las peticiones. Las sesiones tradicionales no funcionan bien en APIs porque rompen el principio de “sin estado” (stateless). La solución moderna es JWT (JSON Web Tokens).

¿Qué es JWT?

JWT (JSON Web Token) es un estándar abierto para transmitir información de forma segura entre dos partes como un objeto JSON. Un token JWT tiene tres partes:

  1. Header: tipo de token y algoritmo de firma
  2. Payload: los datos (claims) — quién es el usuario, cuándo expira
  3. Signature: verifica que el token no fue alterado

Se ven así:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmFtZSI6IjhkZXZteCIsImlhdCI6MTcxMTM5MDQwMH0.Xl2jF_8mQ...

¿Por qué JWT en APIs REST?

En las sesiones tradicionales, el servidor guarda el estado del usuario. En REST queremos que el servidor sea stateless: no guarda nada sobre las sesiones.

Con JWT, el cliente guarda el token y lo envía en cada petición. El servidor solo verifica que el token sea válido y extrae la información del usuario directamente del token. No necesita consultar una base de datos de sesiones.

Instalación de tymon/jwt-auth

composer require tymon/jwt-auth

Publicar el archivo de configuración:

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

Generar la clave secreta del JWT:

php artisan jwt:secret

Esto agrega JWT_SECRET a tu .env.

Configuración

En config/auth.php, cambia el guard predeterminado:

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

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

Modelo y migración de Usuario

En app/Models/User.php:

<?php

namespace App\Models;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Model implements AuthenticatableContract, JWTSubject {
    use Authenticatable;

    protected $fillable = ['nombre', 'email', 'password'];
    
    protected $hidden = ['password'];

    // Métodos requeridos por JWTSubject
    public function getJWTIdentifier() {
        return $this->getKey();
    }

    public function getJWTCustomClaims() {
        return [];
    }
}

Migración básica:

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('nombre');
    $table->string('email')->unique();
    $table->string('password');
    $table->timestamps();
});

AuthController completo

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller {

    // POST /api/registro
    public function registro(Request $request) {
        $this->validate($request, [
            'nombre'   => 'required|string|max:100',
            'email'    => 'required|email|unique:users',
            'password' => 'required|string|min:8',
        ]);

        $user = User::create([
            'nombre'   => $request->nombre,
            'email'    => $request->email,
            'password' => Hash::make($request->password),
        ]);

        $token = auth()->login($user);

        return response()->json([
            'usuario' => $user,
            'token'   => $token,
            'tipo'    => 'bearer',
        ], 201);
    }

    // POST /api/login
    public function login(Request $request) {
        $this->validate($request, [
            'email'    => 'required|email',
            'password' => 'required',
        ]);

        $credenciales = $request->only('email', 'password');

        if (!$token = auth()->attempt($credenciales)) {
            return response()->json([
                'error' => 'Credenciales incorrectas'
            ], 401);
        }

        return response()->json([
            'token'     => $token,
            'tipo'      => 'bearer',
            'expira_en' => auth()->factory()->getTTL() * 60 . ' segundos',
        ]);
    }

    // GET /api/perfil (protegida)
    public function perfil() {
        return response()->json(auth()->user());
    }

    // POST /api/logout (protegida)
    public function logout() {
        auth()->logout();
        return response()->json(['mensaje' => 'Sesión cerrada exitosamente']);
    }

    // POST /api/refresh (protegida)
    public function refresh() {
        return response()->json([
            'token' => auth()->refresh(),
        ]);
    }
}

Proteger rutas con middleware

En routes/web.php:

// Rutas públicas
$router->post('/api/registro', 'AuthController@registro');
$router->post('/api/login', 'AuthController@login');

// Rutas protegidas con JWT
$router->group(['middleware' => 'auth:api'], function () use ($router) {
    $router->get('/api/perfil', 'AuthController@perfil');
    $router->post('/api/logout', 'AuthController@logout');
    $router->post('/api/refresh', 'AuthController@refresh');
    
    // Otras rutas protegidas
    $router->get('/api/usuarios', 'UsuarioController@index');
    $router->get('/api/usuarios/{id}', 'UsuarioController@show');
});

Cómo usar el token en las peticiones:

El cliente debe enviar el token en el header Authorization:

// JavaScript/Fetch
const response = await fetch('https://mi-api.com/api/perfil', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  }
});
GET /api/perfil
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Con JWT en Laravel tienes una API segura, sin estado y lista para consumirse desde cualquier cliente: web, mobile o incluso otros servicios. La implementación puede parecer extensa al inicio, pero una vez configurada funciona de forma transparente en toda tu aplicación.