Las rutas protegidas se agrupan con middleware en un orden concreto:
// routes/api.php
Route::get('/health', HealthController::class); // Public
Route::middleware([
EnsureTokenIsValidMiddleware::class,
'throttle:api-token',
AddRateLimitHeadersMiddleware::class,
LogApiRequestsMiddleware::class,
])->controller(LicenseController::class)->group(function () {
Route::get('/licenses', 'show');
Route::post('/license', 'create');
Route::delete('/license/{license}', 'destroy');
});
El orden importa. El middleware corre de arriba abajo en la petición y de abajo arriba en la respuesta:
Camino de la petición:
- EnsureTokenIsValidMiddleware: valida que el token existe, no ha expirado y el dominio coincide. Si falla, rechaza con 401. Fin.
- throttle:api-token: comprueba si el token superó el límite. Si sí, devuelve 429. Fin.
- AddRateLimitHeadersMiddleware: añade las cabeceras de límite.
- LogApiRequestsMiddleware: encola un job de registro.
- Controlador: ejecuta la lógica del endpoint.
Este orden garantiza que las peticiones fallidas se detienen pronto. Una petición sin token nunca llega al límite de frecuencia. Una petición limitada nunca llega a tu controlador. Eso es eficiente y predecible.
Validacion de dominio: atar tokens a origenes
Los tokens pueden restringir desde qué dominios se les permite usarse. Es crucial en escenarios con socios.
Imagina que emites un token al Socio A. Prometen usarlo sólo desde partner-a.com. Pero ¿y si les roban las credenciales? Un atacante consigue el token pero opera desde attacker.com. Sin validación de dominio, el token sigue funcionando. Con ella, falla.
private function isTokenValidForDomain(Request $request, Bearer $token): bool
{
$domains = $token->domains;
// No domain config? Always allow
if (empty($domains) || ! config('bearer.verify_domains', false)) {
return true;
}
// Handle JSON storage edge case
if (is_string($domains)) {
$decoded = json_decode($domains, true);
if (! is_array($decoded)) {
return false; // Malformed domains data
}
$domains = $decoded;
}
// Check if request origin is whitelisted
return in_array($request->getSchemeAndHttpHost(), $domains, true);
}
getSchemeAndHttpHost() devuelve el origen completo: https://partner-a.com. Compararlo con la lista blanca garantiza que el token sólo funcione desde los dominios esperados.
El flujo completo de una peticion
Client sends: GET /api/licenses
Header: Authorization: Bearer abc123
↓
Router matches route, checks middleware
↓
EnsureTokenIsValidMiddleware:
- Extract token "abc123"
- Cache key: "bearer:abc123"
- Cache hit? Return cached Bearer model
- Cache miss? Query database, cache, return
- Valid? Not expired? Domain matches?
→ If yes, continue
→ If no, return 401
↓
throttle:api-token:
- Check rate limit for token
→ If under limit, continue
→ If over limit, return 429
↓
AddRateLimitHeadersMiddleware:
- Calculate remaining quota
- Add headers: X-RateLimit-*
↓
LogApiRequestsMiddleware:
- Set defer callback to log this request
- Continue to controller
↓
LicenseServiceProvider::boot():
- Inspect token settings
- Set Config to use correct driver
- Set driver credentials from token
↓
LicenseController::show():
- Call LicenseFacade::licenses()
- Returns LicenseDTO[]
- Maps to LicenseResource
- Wraps in CollectionResponse
↓
Response flows back through middleware (bottom to top)
↓
SendToLogsJob executes in queue:
- Write log entry with request details
- Token hashed, not exposed
Todo esto ocurre automáticamente. Ningún código de controlador tiene que pensar en autenticación, límites, registro o configuración. Lo impone el middleware, todo consistente, todo probado.