Ir al contenido principal
Laravel, shipping fast.
Capitulo 3 · Autenticacion

Configuracion dinamica mediante los settings del token

Julian Beaujardin

El LicenseServiceProvider es donde la propiedad settings cobra vida. Cuando llega una petición con un token, el provider examina sus ajustes y configura la aplicación para esa integración concreta.

// app/Providers/LicenseServiceProvider.php
final class LicenseServiceProvider extends ServiceProvider
{
    use CachesBearerTokens;

    public function register(): void
    {
        $this->app->singleton(LicenseManager::class);
    }

    public function boot(): void
    {
        $token = request()->bearerToken();
        if (! $token) {
            return;
        }

        // Get cached Bearer instance (same cache hit as middleware)
        $bearer = $this->rememberBearerToken($token);
        if (! $bearer instanceof Bearer) {
            return;
        }

        // Extract configuration from token settings
        $settings = $bearer->settings;
        if (! is_array($settings)) {
            return;
        }

        $licenseSettings = $settings['license'] ?? null;
        if (! is_array($licenseSettings)) {
            return;
        }

        // Set which driver to use (Statamic, Filament, etc.)
        $driver = $licenseSettings['driver'] ?? null;
        if (! is_string($driver)) {
            return;
        }

        Config::set('services.license.default', $driver);

        // Set driver credentials (e.g., Statamic API token)
        $driverToken = $licenseSettings['token'] ?? null;
        if (is_string($driverToken)) {
            Config::set("services.license.drivers.{$driver}.token", $driverToken);
        }
    }
}

Ambos tokens se autentican contra la misma API. Pero las peticiones del Socio A usan el token de Statamic del Socio A, y las del Socio B el suyo. El mismo LicenseController, el mismo LicenseService, todo igual. Sólo cambia la configuración por token.

Esto es multiinquilinidad sin microservicios, sin complejidad de enrutado y sin tablas de inquilinos en tu base de datos. Está todo en el token.

Middlewares

El middleware es la capa interceptora entre una petición que llega a tu API y su llegada al controlador. Piénsalo como una serie de puertas que la petición debe atravesar. Cada puerta puede examinarla, modificarla, dejarla pasar sin cambios o cerrarse de golpe y devolverla sin que llegue nunca a tu controlador.

Cada middleware hace un trabajo:

  • Validar que existe un token Bearer
  • Comprobar si la petición supera un límite de frecuencia
  • Añadir cabeceras de seguridad a la respuesta
  • Registrar lo que pasó
  • Detectar el idioma preferido del cliente
  • Comprimir el cuerpo de la respuesta

Cada middleware recibe una petición, decide qué hacer con ella y la pasa al siguiente de la cadena. La respuesta vuelve por la misma cadena en orden inverso, así que cada middleware también tiene ocasión de transformar la respuesta antes de que salga. Por eso el orden importa. Una petición rechazada por el middleware de autenticación nunca llega al límite de frecuencia. Una respuesta comprimida al final obtiene las cabeceras de longitud correctas.

Piénsalo como el control de un aeropuerto. Cada punto hace una cosa:

  • Facturación verifica tu billete.
  • Seguridad escanea tu equipaje.
  • Control de pasaportes verifica tu identidad.
  • La puerta de embarque confirma que estás en el vuelo correcto.

Si algún punto te rechaza, no embarcas. No pasas al siguiente. El middleware funciona igual.

En esta API, las rutas protegidas del LicenseController se agrupan con middleware de ruta. El orden es deliberado y refleja cómo debe examinarse la petición:

// routes/api.php
Route::middleware([
    EnsureTokenIsValidMiddleware::class,
    'throttle:api-token',
    AddRateLimitHeadersMiddleware::class,
    LogApiRequestsMiddleware::class,
])->controller(LicenseController::class)->group(function () {
    // routes
});

La ruta /health sigue siendo pública, así que no usa estos middleware de ruta. Sólo recibe el middleware global de API que añadimos más adelante.

EnsureTokenIsValidMiddleware: validacion del token

Toda petición debe demostrar su identidad. No puedes aceptar cualquier token que aparezca en la cabecera Authorization. Tienes que verificarlo, comprobar que no ha expirado y opcionalmente validar que el origen de la petición está permitido. Eso hace EnsureTokenIsValidMiddleware.

Este middleware extiende VerifyBearerToken del paquete de Chandler, añadiendo lógica de validación propia encima de la verificación base:

// app/Http/Middleware/EnsureTokenIsValidMiddleware.php
class EnsureTokenIsValidMiddleware extends VerifyBearerToken
{
    use CachesBearerTokens;

    public function handle(Request $request, Closure $next): Response
    {
        $token = $request->bearerToken();

        if (! is_string($token)) {
            return parent::handle($request, $next);
        }

        $foundToken = $this->rememberBearerToken($token);

        if (! $foundToken ||
            $foundToken->expired ||
            ! $this->isTokenValidForDomain($request, $foundToken)) {
            return parent::handle($request, $next);
        }

        return $next($request);
    }
}

Cuando llega una petición, este middleware hace tres comprobaciones críticas en secuencia.

Primero llama a $request->bearerToken() para extraer el token de la cabecera. Si no hay token, delega de inmediato en el manejador padre, que rechazará con un 401. Es limpio: si no hay token, no hay nada propio que validar.

Después llama a $this->rememberBearerToken($token). Este método del trait CachesBearerTokens mira primero la caché y, si falla, consulta la base de datos y cachea el resultado. Si el token no existe, el middleware rechaza con 401. Si existe, ya tienes una instancia de Bearer con todas sus propiedades disponibles.

Con la instancia en mano, compruebas $foundToken->expired. Es una propiedad calculada que compara expires_at con la hora actual. Si ha expirado, rechazar con 401 es lo correcto. Los tokens expirados no sirven de nada.

Por último, si el token tiene restricciones de dominio, validas que la petición venga de un dominio permitido. Ahí entra isTokenValidForDomain().

Token Extraction and Type Check
Token Lookup and Existence
Expiration Check
Domain Validation

La validación de dominio es opcional pero potente. Los tokens pueden especificar qué dominios pueden usarlos. Es crucial en escenarios multicuenta o con socios, donde quieres asegurarte de que un token emitido a un cliente concreto sólo pueda usarse desde su dominio.

El método isTokenValidForDomain() maneja varios casos límite:

Comprobación de configuración. Primero mira si hay dominios configurados en este token. Si $token->domains está vacío, o si la configuración global bearer.verify_domains está desactivada, se salta la validación. Eso significa que puedes usar el mismo middleware en todas partes, y que se salta con elegancia la comprobación para tokens sin restricciones. Flexibilidad sin complejidad.

Análisis de JSON. Los dominios podrían estar almacenados como una cadena JSON. El middleware intenta decodificarlos: json_decode($domains, true). Si falla o no devuelve un array, el token se rechaza. Esto protege frente a datos malformados.

Coincidencia exacta de dominio. Por último compara el origen de la petición ($request->getSchemeAndHttpHost()) con los dominios permitidos, usando in_array(..., true) con comparación estricta. getSchemeAndHttpHost() devuelve el origen completo incluyendo el protocolo, como https://example.com. Al exigir coincidencia exacta, robar un token es más difícil: un atacante con un token atado a https://example.com no puede pedir desde http://example.com ni desde https://attacker.com.

Si alguna comprobación falla —token ausente, inválido, expirado o dominio incorrecto— el middleware llama a parent::handle($request, $next). Esta es la parte crucial: estás delegando en el middleware VerifyBearerToken de Chandler, que maneja el rechazo correctamente. Devuelve un 401 con las cabeceras adecuadas. Tu lógica propia no tiene que reimplementar el manejo de errores.

Cuando la validación pasa, el middleware llama a return $next($request). La petición llega a tu controlador con el bearer autenticado disponible:

$bearer = Auth::user();  // The authenticated bearer

El bearer queda disponible como el «usuario» autenticado de la petición, aunque no sea un usuario. Internamente Laravel trata a los bearers autenticados como usuarios respaldados por un guard, así que todos los patrones de autenticación conocidos funcionan.

{
    "token": "abc123xyz",
    "expires_at": "2026-12-31",
    "domains": ["https://client-a.com"],
    "settings": {
        "license": {
            "driver": "statamic",
            "token": "statamic_token_for_a"
        }
    }
}
{
    "token": "def456uvw",
    "expires_at": "2026-12-31",
    "domains": ["https://client-b.com"],
    "settings": {
        "license": {
            "driver": "filament",
            "token": "filament_token_for_b"
        }
    }
}

Cuando el Cliente A envía una petición desde https://client-a.com con su token, el middleware:

  1. Encuentra su token en la base de datos ✓
  2. Comprueba que no ha expirado ✓
  3. Verifica que el dominio coincide ✓
  4. Pasa la petición al controlador

Más adelante, la petición del Cliente A llega a LicenseServiceProvider::boot(), que mira los settings del bearer y configura la aplicación para usar Statamic con las credenciales del Cliente A.

Si el Cliente B intentara usar el token del Cliente A desde su propio dominio, la validación de dominio fallaría y la petición sería rechazada. Si el Cliente B usara su propio token pero desde el dominio equivocado, mismo resultado. Esto previene el mal uso de tokens.

Estrategia de revocación

La revocación de tokens es una preocupación habitual. Hay dos enfoques, cada uno con sus compromisos.

Enfoque 1: revocación inmediata (sin caché)

  • Consulta la base de datos en cada petición
  • Los tokens revocados se rechazan al instante
  • Contra: entre dos y tres veces más carga de base de datos, respuestas más lentas
  • Úsalo cuando: la seguridad sea crítica (por ejemplo, tras detectar abuso)

Enfoque 2: revocación basada en caché (este proyecto)

  • Cachea los tokens como hacemos aquí
  • Los tokens revocados siguen siendo válidos hasta que expira la caché
  • Contra: la revocación tarda hasta el TTL en surtir efecto
  • A favor: muchísimas menos consultas, escala a miles de peticiones por segundo
  • Úsalo cuando: puedas tolerar la validez temporal de tokens revocados

Para esta API usamos caché porque el TTL es corto. Si revocas un token ahora mismo, un atacante puede usarlo como mucho unos segundos más, normalmente suficiente para la mayoría de emergencias sin sacrificar el rendimiento.

Si necesitas revocación instantánea, puedes mantener una caché aparte de «tokens revocados» junto a la de bearers, y comprobarla antes de aceptar un bearer cacheado. Eso te da rendimiento y revocación inmediata a la vez.

El trait CachesBearerTokens

Tanto EnsureTokenIsValidMiddleware como LicenseServiceProvider llaman a rememberBearerToken(). Es intencionado: los tokens Bearer se consultan con frecuencia, y las consultas se acumulan rápido. Cacheando el token evitas un golpe a la base de datos en cada petición.

El trait ofrece dos métodos. rememberBearerToken($token) es el que llaman ambos. Envuelve la lógica de caché en una interfaz limpia y reutilizable. calculateBearerCacheTTL($bearer) determina cuánto debe cachearse un token:

  • Para tokens sin expiración: algunos son permanentes. Cachéalos una hora. Es improbable que cambien, y una hora es tiempo suficiente para que merezca la pena y lo bastante corto para olvidarlo.

  • Para tokens con expiración: el TTL es el tiempo restante menos un margen de cinco minutos. El margen es todo el asunto. Cachea un token exactamente el tiempo que es válido y la entrada muere en el mismo instante que el token —y una petición que caiga en esa ventana puede recibir un token vivo en caché y muerto en la base de datos. Restar el margen garantiza que la caché se rinde primero, así que la base de datos siempre emite el voto decisivo.

La clave de caché es siempre bearer:{token}, así que ambos sitios comparten el mismo valor cacheado. La primera búsqueda (normalmente en el middleware) cachea el token y calcula el TTL. La segunda (en el service provider) acierta en caché al instante. Sin consultas adicionales.

Por qué importa: imagina mil peticiones por segundo. Sin caché, eso son dos mil consultas por segundo (una en el middleware, otra en el provider). Con caché, quizá dos o tres por segundo para clientes recién autenticados, y cero para clientes repetidos dentro de la ventana. Es una mejora drástica. Tu base de datos puede respirar.