Índice de contenido
- ¿Qué es una API REST?
- Servidor y cliente
- Qué puede hacer una API REST
- REST y sus reglas
- HTTP y los métodos
- APIs en general
- Códigos de Estado (HTTP Status Codes)
- ¿Qué es un JSON exactamente?
- Instalación de la API
- Creando el API Controller
- Controladores y rutas
- Explicación del código anterior
- Manejar excepciones
- ¿Cómo funciona este flujo?
- Implementar métodos personalizados
- Obtenerlas todas
- Consumir por el slug
- ¿Cuándo UNIFICAR Endpoints en tu REST API? (Optimización Real)
- Identificación del Problema: Múltiples Peticiones Redundantes
- Regra de Oro para la Unificación de Endpoints
- Estrategias de Gestión de Estado y Carga Inicial
- Consideraciones para Control de Versiones y Clientes Móviles
- Estrategias de Protección de una API REST en Laravel con Firma Digital HMAC y Timestamps
- Protección en Entornos Web Mediante CORS
- Protección para Aplicaciones Móviles: HMAC y Timestamps
- 1. Fundamentos Teóricos: HMAC, Integridad y Replay Attacks
- El flujo de firma y verificación
- Flujo del Algoritmo de Autenticación HMAC
- 2. Creación del Middleware de Validación en Laravel
- Paso 1: Crear la clase del Middleware
- Paso 2: Código de la Clase Middleware
- 3. Configuración y Registro del Middleware
- Registrar el Middleware en Laravel 11 / 12
- 4. Protección de Rutas en routes/api.php
- 5. Implementación en el Cliente (Flutter / Dart)
- Paso 1: Instalación de dependencias
- Paso 2: Generación de la Firma HMAC en Dart
- 6. Buenas Prácticas en el Desarrollo del Cliente Móvil
- 7. Buenas Prácticas de Seguridad Adicionales
- Conclusión
Una Rest Api no es más que una interfaz entre sistemas que usa HTTP para obtener y enviar datos o generar operaciones sobre esos datos en varios formatos como XML y JSON.
Para crear una Rest Api, podemos emplear exactamente la misma lógica que manejamos hasta ahora; la única diferencia es donde van a estar registradas nuestras rutas, que ya no estarían en el archivo de web.php si no en el archivo de api.php.
Para este capítulo, vamos a crear un nuevo proyecto en Laravel aunque, puedes emplear el mismo proyecto que hemos empleado hasta ahora, si decides crear un nuevo proyecto, debes de copiar las migraciones, request y modelos de Post y Categoría.
Las API RESTs brindan una forma flexible y liviana de integrar aplicaciones; es decir, en la cual podemos comunicar dos o más aplicaciones.
Veamos los conceptos claves:
Una API es un conjunto de reglas que definen cómo las aplicaciones o dispositivos pueden conectarse y comunicarse entre sí.
Una API REST no es más que una API que se ajusta a los principios de diseño de REST y utiliza las peticiones de HTTP (GET, POST, PUT, PATCH, DELETE) para realizar el consumo y gestión de estos datos.
La arquitectura REST no es más que un conjunto de restricciones o limitaciones entre las principales, tenemos:
- Separación entre el cliente y el servidor; es decir, dos aplicaciones aparte.
- Sin estado, es decir, por buenas prácticas, no debemos usar sesiones o mecanismos similares.
- Cacheable, para hacer más eficiente, podemos guardar en caché la respuesta a un mismo recurso.
- Una interfaz uniforme tanto para el consumo de la misma en la cual cada recurso debe de tener una URI establecida y respuestas devueltas en JSON o XML.
En la práctica, una Rest Api no es más que una aplicación o módulo de la misma, la cual cuenta con un conjunto de funciones implementadas que pueden ser consumidas mediante una URL y las mismas pueden hacer operaciones CRUD para administrar los datos. La Rest Api es consumida mediante peticiones HTTP y siempre devuelven un mismo tipo de dato; JSON principalmente.
Ya conocemos como realizar operaciones CRUD en Laravel con los modelos en Base de Datos, ahora, vamos a aprovechar estos conocimientos para crear una Rest Api.
¿Qué es una API REST?
Te voy a contar una pequeña “historia de abuelo”.
Supongamos que tenemos nuestra súper aplicación en Laravel. Ya tenemos nuestras entidades, podemos crear registros, editarlos, eliminarlos.
Ahora bien, imagina que queremos consumir esa información desde otra aplicación, por ejemplo, una aplicación hecha en Vue, React, Angular, Astro, o cualquiera de los 20 frameworks de JavaScript que existen.
Pero no solo eso. También podría ser una aplicación móvil:
- Android (Android Studio)
- React Native
- Flutter (mi favorita)
- iOS con Swift o SwiftUI
Piensa, por ejemplo, en Gmail: tú puedes ver tus correos desde el navegador, pero también desde el móvil. Lo que está pasando ahí es que la aplicación móvil (el cliente) se conecta a un servidor, y ese servidor expone una API.
Eso es básicamente una API REST: Un mecanismo que permite que distintas aplicaciones se comuniquen entre sí.
Servidor y cliente
En la mayoría de los casos, tenemos:
- Un servidor, que en nuestro caso será Django.
- Un cliente, que puede ser una aplicación web o una app móvil.
Aunque normalmente conectamos servidor con cliente, también podrías consumir una API REST desde otra aplicación en Django, o desde Laravel, Flask, etc. En resumen: una API REST nos permite interconectar aplicaciones, sin importar la tecnología que usen.
Qué puede hacer una API REST
Una API REST no sirve solo para crear, leer, actualizar o eliminar datos. También puede:
- Enviar correos
- Ejecutar procesos
- Automatizar tareas
- Exponer servicios a terceros
Todo es programable.
La idea clave que quiero que te quedes es esta:
Una API REST es un mecanismo para conectar aplicaciones distintas, normalmente un servidor con uno o varios clientes.
REST y sus reglas
Una API REST no es solo “devolver datos”. Tiene reglas.
Por ejemplo:
- GET → consultar datos
- POST → crear registros
- PUT / PATCH → actualizar (total o parcialmente)
- DELETE → eliminar
Aunque técnicamente podrías usar GET para crear datos o POST para consultar, no es lo recomendado, por temas de seguridad y buenas prácticas.
REST es, básicamente, un conjunto de normas que nos dicen cómo debemos hacer esa comunicación.
HTTP y los métodos
Aquí hay algo importante:
HTML solo entiende GET y POST, pero el protocolo HTTP (el que usamos cuando navegamos por internet, con HTTP o HTTPS) soporta muchos más métodos:
- GET
- POST
- PUT
- PATCH
- DELETE
Y precisamente las APIs REST se basan en HTTP, no en HTML.
APIs en general
Una API es un interfaz de programación de aplicaciones. Existen muchos tipos de APIs:
- SOAP
- GraphQL
- REST
Todas sirven para comunicar aplicaciones, pero cada una tiene sus propias reglas, ventajas y desventajas, igual que pasa cuando comparas Django con Laravel.
Códigos de Estado (HTTP Status Codes)
En una API, ya no devolvemos una página de error visual, sino un código numérico que la aplicación cliente (Vue, Flutter, etc.) debe interpretar:
- 200 OK: Todo salió bien.
- 201 Created: El registro se creó con éxito.
- 400 / 422: Error del cliente (datos mal enviados o validación fallida).
- 401 Unauthorized: El usuario no ha enviado un token válido.
- 404 Not Found: El recurso no existe.
- 500 Internal Server Error: Nuestro servidor Laravel explotó por un error de lógica.
¿Qué es un JSON exactamente?
Si nunca has visto uno a fondo, es simplemente un formato de texto basado en pares clave-valor.
- Si es un solo objeto, usamos llaves {}.
- Si es una lista de datos (como el index), usamos corchetes [] para representar un array.
Ejemplo de lo que devuelve tu nueva ruta:
[
{
"id": 1,
"title": "Laravel 11",
"slug": "laravel-11"
},
{
"id": 2,
"title": "Vue.js",
"slug": "vue-js"
}
]Instalación de la API
A partir de Laravel 11, el archivo de api.php no se encuentra publicado, para publicarlo, debemos de ejecutar los comandos de artisan:
$ php artisan install:apiEl archivo api.php contiene las rutas para la creación de una Api Rest; estas rutas están diseñadas para no tener estado, por lo que las solicitudes que ingresan a la aplicación a través de estas rutas deben autenticarse mediante tokens y no tendrán acceso al estado de la sesión.
Con esto, se publicará el archivo de api.php:
routes\api.php
Y se instalará Sanctum en el proceso que es un paquete para habilitar la autenticación que trataremos más adelante:
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Downloading laravel/sanctum (vX.X)
- Installing laravel/sanctum (vX.X): Extracting archiveYa con esto, para acceder a las rutas, debemos de colocar la URL del dominio seguido del prefijo de api:
<DOMAIN>/api/<RESOURCE>Por ejemplo:
http://larafirststeps.test/api/category
Si quieres personalizar las rutas para indicar otro prefijo que no sea el de API:
use Illuminate\Support\Facades\Route;
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
then: function () {
Route::middleware('api')
->prefix('webhooks')
->name('webhooks.')
->group(base_path('routes/webhooks.php'));
},
)Más información en:
https://laravel.com/docs/master/routing#routing-customization
Recuerda ejecutar las migraciones en caso de que existan:
$ php artisan migrateCreando el API Controller
En una REST API dejamos de devolver HTML (vistas Blade) para devolver JSON, que es el formato estándar, ligero y fácil de leer para las máquinas. Para mantener el orden, guardaremos estos controladores en una carpeta específica:
app/Http/Controllers/Api.
Controladores y rutas
Creamos los controladores para las APIs:
$ php artisan make:controller Api/PostController -m PostY
$ php artisan make:controller Api/CategoryController -m CategoryCreamos las rutas en:
routes/api.php:
Route::resource('category', App\Http\Controllers\Api\CategoryController::class)->except(["create", "edit"]);
Route::resource('post', App\Http\Controllers\Api\PostController::class)->except(["create", "edit"]);Para evitar conflictos entre los nombre de las rutas en el dashboard y las de API, vamos a colocarle un prefijo a los nombre de las rutas de la API:
routes/api.php:
Route::group(['as' => 'api.'], function () {
Route::resource('category', CategoryController::class)->only(['index']);
Route::resource('post', PostController::class)->only(['index']);
});Los controladores lucen como:
Api/CategoryController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Category\PutRequest;
use App\Http\Requests\Category\StoreRequest;
use App\Models\Category;
use Illuminate\Http\JsonResponse;
class CategoryController extends Controller
{
public function index(): JsonResponse
{
return response()->json(Category::paginate(10));
}
public function store(StoreRequest $request): JsonResponse
{
return response()->json(Category::create($request->validated()));
}
public function update(PutRequest $request, Category $category): JsonResponse
{
$category->update($request->validated());
return response()->json($category);
}
public function destroy(Category $category): JsonResponse
{
$category->delete();
return response()->json(['message' => 'Deleted'], 204);
}
}Y para el de Post:
Api/PostController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Post\PutRequest;
use App\Http\Requests\Post\StoreRequest;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
class PostController extends Controller
{
public function index(): JsonResponse
{
return response()->json(Post::paginate(10));
}
public function store(StoreRequest $request): JsonResponse
{
return response()->json(Post::create($request->validated()));
}
public function show(Post $post): JsonResponse
{
return response()->json($post);
}
public function update(PutRequest $request, Post $post): JsonResponse
{
$post->update($request->validated());
return response()->json($post);
}
public function destroy(Post $post): JsonResponse
{
$post->delete();
return response()->json(['message' => 'Deleted'], 204);
}
}Explicación del código anterior
Puedes ver que prescindimos de algunos métodos como el de los formularios de edit y create; ya que, en una Api Rest, no es necesario estas vistas intermedias para crear los recursos, recordemos que, estas son empleadas para pintar el formulario y nada más, y en una Rest Api, esto no sería necesario, solamente mantener los procesos de crear y editar.
Finalmente, siempre devolvemos una respuesta en formato JSON con: response()->json().
La cual recibe dos parámetros:
- Los datos.
- El código de estado.
Para exponer la categoría con toda la información y no solo el identificador:
{
"id": 1,
"title": "Post 1",
"slug": "post-1",
"description": "test",
"content": "test",
"image": "test",
"posted": "yes",
"category_id": 1,
"created_at": null,
"updated_at": null,
"category": {
"id": 1,
"title": "cate 1 new",
"slug": "cate-1"
}
}Podemos indicar que traiga la relación al momento de hacer la paginación:
Api/PostController.php
public function index()
{
return response()->json(Post::with('category')->paginate(10));
}Aunque response()->json() devuelve por defecto un estado 200 (OK), las buenas prácticas sugieren ser más específicos:
- 201 (Created): Ideal para el método store, indicando que el recurso se creó con éxito.
- 204 (No Content): Es el estándar para el método destroy. Significa que la operación fue exitosa pero no hay contenido que mostrar (porque el registro ya no existe).
Te recomiendo familiarizarte con los códigos de estado HTTP.
Manejar excepciones
Para manejar las excepciones, específicamente aquellas que ocurren cuando no existen los registros al momento de la búsqueda, por ejemplo:
"message": "No query results for model [App\\Models\\Category] cate-1asas",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",Si cambias la variable APP_DEBUG=false en tu archivo .env, Laravel dejará de mostrar esos detalles técnicos y mostrará una página de error genérica. Sin embargo, para una API, queremos un control más fino.
A partir de Laravel 11, tenemos el manejo de las configuraciones globales en un solo archivo:
bootstrap\app.php
Así que, desde el método de:
withExceptionsManejamos las excepciones, desde el mencionado método podemos capturar las excepciones que queremos personalizar:
bootstrap\app.php
return Application::configure(basePath: dirname(__DIR__))
***
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (NotFoundHttpException $e, $request) {
if($request->expectsJson()){ // or $request->wantsJson()
return response()->json('Not found',404);
}
});
})->create();Especificamos un manejo de excepción específico para la excepción que está ocurriendo que es la de:
Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpExceptionY si se espera recibir una respuesta JSON ($this->expectsJson()) el cual es el formato que vamos a emplear desde la Api Rest; y en este caso, generamos una excepción personalizada como la que implementamos anteriormente. Desde el archivo anterior, puedes personalizar el comportamiento de cualquier otra excepción que consideres.
¿Cómo funciona este flujo?
- Captura: El método render detecta específicamente la excepción que definas (puedes hacer Control+Click en la clase para ver todas las que Laravel ofrece en la carpeta Vendor).
- Discriminación: Usamos $request->expectsJson(). Esto es vital porque si el usuario está navegando en el Dashboard (web) y algo no se encuentra, queremos que vea la página 404 de Blade, no un código JSON.
- Respuesta: Si es una petición de API (gracias al header Accept: application/json que configuramos en Postman), devolvemos nuestra respuesta personalizada.
Implementar métodos personalizados
En este apartado, vamos a crear algunos métodos específicos para el consumo de los posts o categorías.
Obtenerlas todas
Ahora, vamos a crear un par de métodos para obtener todos los registros sin paginación:
app\Http\Controllers\Api\PostController.php
public function all(): JsonResponse
{
return response()->json(Post::get());
}Y
app\Http\Controllers\Api\CategoryController.php
public function all(): JsonResponse
{
return response()->json(Category::get());
}Las rutas:
routes\api.php
Route::get('post/all', [PostController::class, 'all']);
Route::get('category/all', [CategoryController::class, 'all']);Consumir por el slug
Para consumir por el slug, podemos usar directamente el de show, pero, variando el parámetro en la URL, que NO sea el ID que es el por defecto si no el campo de slug:
Y para las URL, algo como las siguientes:
routes\api.php
Route::get('post/slug/{post:slug}', [App\Http\Controllers\Api\PostController::class, 'show']);
Route::get('category/slug/{category:slug}', [App\Http\Controllers\Api\CategoryController::class, 'show']);Puedes variar la URL, pero es importante que no cause conflicto con otra ya existente, por ejemplo, la de show.
Si consumimos el método anterior, tendremos algo como lo siguiente:
// http://larafirststeps.test/api/post/slug/xgyxsfyabgyefiaubhog
{
"id": 1,
"title": "xGYxsFYABgyEFiAuBhOg",
"slug": "xgyxsfyabgyefiaubhog",
"description": "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Vitae ",
"content": "<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Vitae aperiam culpa veritatis quasi laudantium mollitia quidem est blanditiis ullam illum cupiditate suscipit, quia, itaque quaerat? Iure debitis laudantium aliquam maxime!</p>",
"image": null,
"posted": "yes",
"created_at": "2026-03-14T18:20:14.000000Z",
"updated_at": "2026-03-14T18:20:14.000000Z",
"category_id": 11,
"category": {
"id": 11,
"title": "Categoria 10",
"slug": "categoria-10",
"created_at": "2026-03-14T18:20:14.000000Z",
"updated_at": "2026-03-14T18:20:14.000000Z"
}
}Si quieres que traiga la categoría asociada, puedes usar el esquema de:
$post = Post::with("category")->where("slug", $slug)->firstOrFail();O
$post = Post::where("slug", $slug)->firstOrFail();
$post->category;Importante notar el segundo caso, Laravel trabaja con un esquema lazy loading, lo que significa, es que, no va a traer los datos de relaciones al menos que los solicites; en el segundo caso, estamos consumiendo la categoría del post seleccionado y por ende, realiza la consulta a la base de datos y queda registrado en el objeto de post.
El método firstOrFail() trae un único registro según la condición (al igual que el método de firts()), si no lo encuentra, entonces da un error 404.
Otra variación para el caso anterior, es definir el método de la siguiente manera:
public function slug(Post $post): JsonResponse // $slug
{
//$post = Post::with("category")->where("slug", $slug)->firstOrFail();
$post->category;
return response()->json($post);
}Importante notar que, ahora tenemos el post inyectado en la método (es decir, como parámetro, a esto se le conoce como inyección de dependencia) por lo tanto, para indicar a Laravel que lo que va a recibir es el slug y que haga el mapeo al post; esto, lo indicamos por las rutas:
Route::get('post/slug/{post:slug}', [PostController::class, 'slug']);Para las categorías, vamos a realizar el mismo procedimiento:
public function slug(Category $category): JsonResponse
{
return response()->json($category);
}Y la ruta:
Route::get('category/slug/{category:slug}', [CategoryController::class, 'slug']);Código fuente:
https://github.com/libredesarrollo/book-course-laravel-base-api-11/releases/tag/v0.1
¿Cuándo UNIFICAR Endpoints en tu REST API? (Optimización Real)
En este apartado, quiero hablarte sobre como cuidar tu Rest Api, algunas diretrices que debes de tener en cuenta para que tu RestAPI NO se salga de control y sea mantenible unificando endpoints o que tomes estas recomendaciones para cuando quieras crear un nuevo endpoint.
Una de las optimizaciones fundamentales al desarrollar aplicaciones que consumen una REST API es la gestión eficiente de las consultas HTTP. A medida que una plataforma evoluciona y suma nuevas funcionalidades, existe la tendencia de crear endpoints independientes para cada módulo. Sin embargo, para mantener una arquitectura limpia y de alto rendimiento, es necesario analizar la aplicación como un todo unificado.
Identificación del Problema: Múltiples Peticiones Redundantes
Un escenario habitual en aplicaciones web es la dispersión de peticiones durante la carga inicial. Por ejemplo, al cargar una interfaz de usuario, es común encontrar peticiones independientes para:
- Obtener el perfil del usuario (suscripción, datos de cuenta, token).
- Consultar el listado o contador de notificaciones no leídas.
- Obtener el estado actual del carrito de compras.
Aunque cada componente UI maneje su lógica con variables locales o gestores de estado, realizar múltiples llamadas HTTP consecutivas para datos que se requieren en el mismo ciclo de vida genera una sobrecarga innecesaria tanto en el servidor como en el cliente.
Regra de Oro para la Unificación de Endpoints
Como principio de arquitectura de software, se debe aplicar la siguiente regla: Si un conjunto de datos siempre viaja junto en el mismo ciclo de inicialización de la página, debe unificarse en una sola petición HTTP.
Refactorizar la API para que un único endpoint devuelva la información del usuario junto a sus notificaciones y el carrito de compras reduce la latencia, consolida las llamadas y simplifica la inicialización del estado en el cliente.
Estrategias de Gestión de Estado y Carga Inicial
El problema de unificar endpoints, es que, desde la app, seguramente se consumen estos endpoints desde distintos módulos de la app, por ejemplo, las notificaciones o el carrito de compras son MODULOS apartes pero estamos trayendo los datos unificados en un SOLO endpoints que le tenemos que compartir estos datos que antes resolvían por ellos mismos en un endpoints exclusivo.
Existen diversas alternativas para disponibilizar información básica antes o durante la ejecución de las peticiones de la API:
- Inyección en la ventana global (Window Object): Compartir un objeto inicial renderizado por el backend desde el servidor permite una lectura inmediata para componentes visuales de solo lectura.
- Gestores de Estado (Pinia / Vuex): Al recibir la respuesta del endpoint unificado, los datos se distribuyen en el almacén global para que componentes independientes (como el encabezado o la barra lateral) puedan consumirlos sin realizar peticiones adicionales.
- Persistencia Local y Cookies: Utilizadas para almacenar identificadores de sesión o configuraciones rápidas, aunque con un acceso de lectura más lento en comparación con la memoria del estado global.
Consideraciones para Control de Versiones y Clientes Móviles
Al refactorizar y consolidar endpoints en la REST API, es crucial evaluar el impacto en otros clientes del ecosistema, como aplicaciones móviles creadas en Flutter o React Native.
Si se decide eliminar o modificar un endpoint antiguo, no debe removerse de inmediato en producción. Los usuarios de aplicaciones móviles no siempre actualizan la aplicación instantáneamente; eliminar un endpoint de forma abrupta provocará errores de red (404) y fallos en las versiones instaladas. La estrategia correcta exige mantener el endpoint deprecated durante un tiempo prudencial antes de su eliminación definitiva.
Veamos como proteger la Rest API empleando Laravel Sanctum.
Estrategias de Protección de una API REST en Laravel con Firma Digital HMAC y Timestamps
Una vez implementada una REST API, el siguiente paso fundamental es garantizar su seguridad. Salvo que esté concebida como un servicio público, la gran mayoría de las APIs deben estar restringidas para ser consumidas de forma exclusiva por nuestras propias aplicaciones, evitando accesos no autorizados, consumo indebido de recursos o extracción masiva de datos.
Protección en Entornos Web Mediante CORS
En aplicaciones web tradicionales, el mecanismo primario y obligatorio de protección se basa en la configuración de políticas CORS (Cross-Origin Resource Sharing). Este método define explícitamente cuáles dominios tienen autorización previa para realizar peticiones hacia la API.
Si una petición proviene de un origen no incluido en la lista blanca del servidor, la solicitud es rechazada automáticamente con un código de estado HTTP 403 Forbidden. No obstante, aunque CORS funciona de manera estricta en navegadores web, no ofrece protección frente a clientes que no respetan estas políticas, como las aplicaciones móviles o las herramientas de pruebas HTTP.
En el caso de Laravel, el archivo es config/cors.php
Protección para Aplicaciones Móviles: HMAC y Timestamps
A diferencia del entorno web, las aplicaciones móviles (desarrolladas en Flutter, Kotlin, Swift o frameworks similares) no operan bajo el concepto de un dominio de origen. Para validar que las solicitudes provienen únicamente de una aplicación móvil legítima y que los datos no han sido alterados en tránsito, se debe implementar una firma digital con HMAC (Hash-based Message Authentication Code) combinada con un Timestamp.
Este estándar de seguridad se fundamenta en tres pilares principales:
- Clave Secreta Compartida (Secret Key): Una cadena confidencial almacenada únicamente dentro del cliente móvil y en el backend, la cual nunca viaja explícitamente a través de la red.
- Carga Útil (Payload) y URI: Los datos de la petición (como el endpoint solicitado y sus parámetros) que se utilizan para calcular la firma. Esto evita ataques de manipulación de datos, impidiendo que un atacante altere identificadores dentro de la petición.
- Timestamp y Tolerancia de Caducidad: Se incluye el tiempo actual (en milisegundos Unix) en la firma. El servidor valida que la petición haya sido generada dentro de un rango de tolerancia específico (por ejemplo, 30 a 300 segundos), bloqueando posibles ataques de denegación o retransmisión (replay attacks).
Para garantizar una comunicación íntegra, auténtica e inalterable, la estándar en la industria es la implementación de firmas digitales HMAC-SHA256 combinadas con marcas de tiempo (timestamps). En esta guía completa aprenderás la teoría detrás de este patrón de seguridad y cómo implementarlo paso a paso con un middleware personalizado en Laravel.
Es una firma digital dinámica (HMAC). En lugar de enviar una "contraseña" estática por la red que cualquiera pueda interceptar y copiar en Postman, lo que haces es firmar el sobre de la petición en tiempo real.
HMAC (Hash-based Message Authentication Code) es un mecanismo de autenticación de mensajes que combina una función hash criptográfica (como SHA-256) con una clave secreta compartida.
Su objetivo principal es garantizar dos cosas en una transmisión de datos:
- Autenticidad: Confirmar que el mensaje proviene de quien dice ser (solo quien conoce la clave secreta pudo haberlo generado).
- Integridad: Asegurar que los datos no han sido alterados o manipulados en el camino.
1. Fundamentos Teóricos: HMAC, Integridad y Replay Attacks
El esquema de firma de peticiones HTTP repose en la técnica de secreto compartido (Shared Secret). La aplicación cliente (Flutter, Vue, React, etc.) y el servidor backend poseen una clave secreta que jamás viaja a través de la red en texto plano.
Este sistema proporciona tres capas de defensa fundamentales:
- Integridad de los datos: Garantiza que la ruta o el contenido no hayan sido alterados durante el tránsito.
- Autenticidad del origen: Confirma que la solicitud proviene de un cliente legítimo que posee la clave secreta.
- Protección contra Replay Attacks: El timestamp fija la caducidad de la petición, impidiendo que solicitudes capturadas sean reejecutadas minutos o días después.
El flujo de firma y verificación
- En el Cliente:
- Genera la estampa de tiempo actual en milisegundos (
timestamp). - Construye la cadena base o
payload(ejemplo:$timestamp . $formattedPath). - Calcula el hash encriptado usando HMAC-SHA256 con la clave secreta.
- Adjunta la firma y el timestamp en las cabeceras HTTP (
X-SignatureyX-Timestamp).
- Genera la estampa de tiempo actual en milisegundos (
- En el Servidor (Middleware):
- Intercepta la petición y extrae las cabeceras.
- Valida que la diferencia de tiempo entre el servidor y el
timestamprecibido esté dentro de un margen aceptable (ej. 300 segundos). - Normaliza la ruta entrante y reconstruye exactamente el mismo
payload. - Calcula la firma HMAC localmente y la compara de forma segura contra la firma recibida.
En resumen:
Tanto Laravel como Flutter conocen en secreto una misma palabra clave (llamada $secret). Cuando la app de Flutter va a hacer una petición, realiza la siguiente fórmula matemática rápida:
Firma = HMAC-SHA256}(Fecha} + Ruta + Datos, $secret)
- La app calcula la firma en ese instante y la mete en la cabecera (X-Signature).
- Envía el paquete: La petición viaja con la fecha, la ruta y la firma, pero nunca se transmite el $secret.
- Laravel recibe el paquete: Hace el mismo cálculo en su servidor usando los datos que le llegaron.
- Si la firma que calculó Laravel coincide con la que envió Flutter → La petición viene de tu app legítima.
- Si cambia un solo carácter o la fecha difiere por más de 30 segundos → Devuelve 403 Forbidden.
Flujo del Algoritmo de Autenticación HMAC
- En el cliente móvil (ej. Flutter):
- Se obtiene el Timestamp universal actual.
- Se construye la cadena base (Payload) uniendo el Path del endpoint, el Timestamp y cualquier parámetro adicional.
- Se genera el Hash firmado aplicando un algoritmo criptográfico (como SHA-256) sobre la cadena base junto a la clave secreta.
- Se envían la firma calculada y el Timestamp dentro de las cabeceras HTTP (Headers) de la solicitud.
- En el servidor backend (ej. Middleware en Laravel):
- El Middleware intercepta la petición entrante y extrae las cabeceras HTTP correspondientes.
- Se verifica que el Timestamp se encuentre dentro del margen de tolerancia permitido.
- Se reconstruye el Payload utilizando el Path de la petición recibida y la clave secreta almacenada en la configuración del servidor.
- Se calcula el Hash localmente y se compara con la firma recibida. Si coinciden, la petición continúa su flujo; de lo contrario, se retorna un error de autenticación
401 Unauthorized.
2. Creación del Middleware de Validación en Laravel
Implementaremos la validación en un middleware personalizado que procesará las solicitudes antes de que alcancen los controladores.
Paso 1: Crear la clase del Middleware
Ejecuta el siguiente comando en tu terminal para generar la estructura:
php artisan make:middleware ValidateApiSignaturePaso 2: Código de la Clase Middleware
Abre el archivo app/Http/Middleware/ValidateApiSignature.php e implementa la siguiente lógica:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckAppSignature
{
public function handle(Request $request, Closure $next): Response
{
// 1. Obtener el origen de la petición (lo envía el navegador desde Vue/Axios)
$origin = $request->header('Origin') ?? $request->header('Referer');
// Cargar dinámicamente el array 'allowed_origins' definido en config/cors.php
$allowedOrigins = config('cors.allowed_origins', []);
// dd('/' . ltrim($request->path(), '/'));
// 2. Si la petición viene de un origen Web permitido, bypass a la firma HMAC
if ($origin) {
foreach ($allowedOrigins as $allowed) {
if (str_starts_with($origin, $allowed)) {
return $next($request);
}
}
}
// 3. Si NO viene de la Web (ej. App Móvil Flutter), se EXIGE la firma HMAC
$timestamp = $request->header('X-Timestamp');
$signature = $request->header('X-Signature');
$secret = config('app.mobile_app_secret');
if (!$timestamp || !$signature) {
return response()->json([
'message' => 'Acceso no autorizado: Petición fuera del ecosistema.'
], 403);
}
// Prevenir Replay Attacks (30 segundos de vigencia)
if (abs(time() - (int) $timestamp) > 30) {
return response()->json([
'message' => 'Petición expirada.'
], 403);
}
// Reconstruir Payload y verificar Firma
$path = '/' . ltrim($request->path(), '/');
$payload = $timestamp . $path;
$expectedSignature = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expectedSignature, $signature)) {
return response()->json([
'message' => 'Firma de aplicación inválida.'
], 403);
}
return $next($request);
}
}3. Configuración y Registro del Middleware
Agrega la clave secreta compartida dentro de tu archivo de variables de entorno .env:
API_SECRET_KEY=tu_clave_secreta_super_segura_32_caracteresLuego, edita config/app.php para mapear la variable:
'api_secret_key' => env('API_SECRET_KEY'),Registrar el Middleware en Laravel 11 / 12
En las estructuras modernas de Laravel, añade el alias de tu middleware dentro del archivo bootstrap/app.php:
use App\Http\Middleware\ValidateApiSignature;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'signed.api' => ValidateApiSignature::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
4. Protección de Rutas en routes/api.php
Para aplicar la protección, simplemente asigna el middleware signed.api a tus endpoints. Puedes combinarlo con Sanctum para exigir tanto un usuario autenticado como una firma válida:
use App\Http\Controllers\Api\V1\VideoController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth:sanctum', 'signed.api'])->group(function () {
Route::get('/v1/tutorial/video/protect/vimeo/{id}', [VideoController::class, 'getVimeoStreamUrl']);
});
5. Implementación en el Cliente (Flutter / Dart)
Para firmar digitalmente las peticiones desde nuestra app móvil en Flutter, utilizaremos el paquete oficial de criptografía de Dart.
Paso 1: Instalación de dependencias
Añade el paquete crypto ejecutando el siguiente comando en la raíz de tu proyecto Flutter:
flutter pub add cryptoPaso 2: Generación de la Firma HMAC en Dart
A continuación, se muestra una clase auxiliar o método en Flutter para construir el payload, generar el hash HMAC-SHA256 y realizar la petición enviando las cabeceras requeridas:
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
class ApiClient {
static const String _secretKey = 'tu_clave_secreta_super_segura_32_caracteres';
static const String _baseUrl = 'www.desarrollolibre.net';
/// Genera la firma digital HMAC-SHA256
static String generateSignature(String payload) {
final keyBytes = utf8.encode(_secretKey);
final payloadBytes = utf8.encode(payload);
final hmac = Hmac(sha256, keyBytes);
final digest = hmac.convert(payloadBytes);
return digest.toString();
}
/// Ejemplo de petición GET protegida
static Future<http.Response> getProtectedVideo(String videoId, String userToken) async {
final String relativePath = 'api/v1/tutorial/video/protect/vimeo/$videoId';
// 1. Obtener timestamp en milisegundos
final String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
// 2. Normalizar la ruta (asegurar '/' al inicio y eliminar '/' al final)
final String formattedPath = (relativePath.startsWith('/') ? relativePath : '/$relativePath')
.replaceAll(RegExp(r'/$'), '');
// 3. Crear el payload a firmar
final String payload = '$timestamp$formattedPath';
// 4. Calcular la firma HMAC
final String signature = generateSignature(payload);
// 5. Construir la URI y realizar la solicitud con las cabeceras de seguridad
final Uri url = Uri.https(_baseUrl, relativePath);
return await http.get(
url,
headers: {
'Authorization': 'Bearer $userToken',
'X-Timestamp': timestamp,
'X-Signature': signature,
'Accept': 'application/json',
},
);
}
}6. Buenas Prácticas en el Desarrollo del Cliente Móvil
Para mantener la mantenibilidad del código cuando la aplicación crece y realiza decenas de peticiones a la API, la lógica de generación de cabeceras HMAC no debe repetirse manualmente en cada llamada HTTP. Se deben aplicar principios de Orientación a Objetos mediante la creación de un cliente HTTP centralizado o wrapper:
- Centralización del Cliente HTTP: Implementar una clase base encarga de gestionar los métodos estándar (
GET,POST,PUT,DELETE). - Inyección Automática de Cabeceras: Un único método privado dentro de la clase base debe calcular el Timestamp, procesar la firma HMAC e inyectar las cabeceras requeridas antes de despachar cualquier solicitud.
- Variables de Entorno Cifradas: Guardar la clave secreta en archivos de configuración protegidos o variables de entorno del cliente para evitar su exposición directa en el código fuente.
7. Buenas Prácticas de Seguridad Adicionales
- Uso estricto de HTTPS: Cifra la capa de transporte para evitar que un sniffer lea las cabeceras HTTP o los tokens de sesión.
- Uso obligatorio de
hash_equals(): Evita el uso de operadores condicionales estándar (==o===). La funciónhash_equals()compara cadenas en tiempo constante, mitigando vulnerabilidades de Timing Attacks. - Rotación de claves: Mantén un esquema para actualizar la clave secreta del servidor de forma periódica.
Conclusión
La combinación de CORS para la capa web y HMAC con Timestamps para clientes móviles proporciona un esquema de seguridad robusto, modular y escalable. Esta arquitectura protege los endpoints contra la suplantación de origen, la manipulación de parámetros en tránsito y los ataques de retransmisión, garantizando la integridad total de la REST API.
Implementar firmas HMAC combinadas con marcas de tiempo en Laravel crea un esquema robusto de defensa en profundidad. Con esta solución, cualquier petición modificada o fuera de tiempo será descartada automáticamente por el middleware, garantizando la máxima seguridad para los servicios REST de tu plataforma.