Auth en Serverless: el Agente Smith que verifica tokens antes de decir 'Mr. Anderson...'
Implementar autenticación y autorización suele parecer tan complejo como descifrar el código verde de Matrix. Pero hoy lo simplificaremos con una analogía inolvidable:
Tu API es la mismísima Matrix, los usuarios que están ahí son los humanos despiertos que intentan conectarse y la autorización es el Agente Smith que bloquea cada request con un "Mr. Anderson... muestre su token o esta conversación terminará con un 401".
Capítulo 1: El Agente Smith de la Autenticación: Protegiendo tu API Serverless como la Matrix
En el mundo de serverless (y de los sistemas en general), el Agente Smith es este guardián implacable que cumple una función vital: es la barrera que impide que cualquier entidad no autorizada (scripts maliciosos, bots automatizados o hackers) deambule libremente por tus endpoints como Neo en el edificio del lobby.
Así que prepárate: convertiremos un simple validador de tokens en el Agente Smith de tus APIs serverless. Porque, al final, toda Matrix necesita alguien que vigile las puertas.
Capítulo 2: De Humanos y Middleware: El Primer Paso para Blindar tu Endpoint
Retomaremos el código base de nuestro artículo sobre rate limits (ese T-800 que protege tus APIs), así que asumiremos que ya conoces los fundamentos de middlewares. Si necesitas un refresh, aquí tienes el manual de entrenamiento:

Este es nuestro endpoint desprotegido, tan vulnerable como el primer encuentro de Neo contra los agentes:
const handlerCreateHuman: Handler = async event => {
try {
const createHuman = event.body as Human;
const human = await humanService.create(createHuman);
return formatJSONResponse(201, human);
} catch (error) {
// Validaciones...
}
};
export const handler = middy(handlerCreateHuman)
.use(validateBody()) // Valida el body con Zod
.use(rateLimit()); // Limita las solicitudes con rate-limit-flexible
Aunque tiene defensas básicas utilizando middlewares: una validación en el body por medio de validateBody() y otro para evitar múltiples llamadas de red rateLimit(), esto no es suficiente para mantenernos protegidos.
Capítulo 3: "Mr. Anderson... Presente Su Token."
Antes de que nuestro Agente Smith (middleware) pueda interrogar los requests, necesitamos equiparlo con las herramientas de la resistencia para validar los headers:
$ npm i zod @middy/core @middy/http-header-normalizer
$ npm i -D @types/aws-lambda # Solo para typescript3.1: Creando el detector de intrusos
Nuestro schema Zod actuará como el escáner de humanos, permitiéndonos validar que los headers recibidos tengan un bearer token en authorization:
import { z } from "zod";
export const HeadersSchema = z
.object({
authorization: z
.string()
.regex(/^Bearer \S+$/, "Parece que alguien olvidó su token... qué lástima, Mr. Anderson")
})
.readonly();3.2: El Interrogatorio Inicial
Implementamos la primera capa de seguridad creando un middleware que utilice HeadersSchema:
export const validateHeaders = () => ({
before: async request => {
const validateData = HeadersSchema.parse(request.event.headers);
request.event.headers = validateData;
// Aquí podría ir la lógica de validación
},
});3.3: Fortificando la Matrix
Añadimos nuestro middleware al handler:
export const handler = middy(handlerCreateHuman)
.use(httpHeaderNormalizer()) // Normaliza headers (¡Case-insensitive como en el RFC 2616!)
.use(validateHeaders()) // Primera línea de defensa
.use(validateBody())
.use(rateLimit());
Con esto, nos aseguramos que siempre que llegue un header, tenga sí o sí un authorization con Bearer token, de lo contrario nunca entrará a nuestro handler.
Ahora, la gran pregunta "¿Por qué httpHeaderNormalizer?": Este es nuestro traductor universal que convierte Authorization, AUTHORIZATION y authorization a formato estándar para evitar que los hackers jueguen con mayúsculas y minúsculas. Es un estándar definido en la RFC 2616 (lo dejaré en las referencias).
Capítulo 4: El juicio del Token
Nuestro Agente Smith (middleware) intercepta las requests, pero ahora necesitamos el Oráculo de la Autenticación (un endpoint externo) para determinar quién es realmente "El Elegido", es decir, quién puede o no entrar a nuestro handler. Primero, armamos nuestro kit:
$ npm i axios4.1: El Oráculo de la Validación
Simularemos un endpoint de autenticación en https://api.matrix.fake/me que nos dirá si el token es válido retornando un usuario, o retornando un error 401 caso contrario.
La interfaz del usuario será la siguiente:
export interface User {
id: string;
email: string;
role: string;
}4.2: La Prueba de Fuego
Implementamos la validación en el handler:
const handlerCreateHuman: Handler = async event => {
try {
const { authorization } = event.headers;
const authResponse = await axios.get("https://api.matrix.fake/me", {
headers: authorization
});
const user = authResponse.data as User;
const createHuman = event.body as Human;
const human = await humanService.create(createHuman);
return formatJSONResponse(201, human);
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401)
return formatJSONResponse(401, {
message: "Token inválido. No eres El Elegido."
}
);
// Otras Validaciones...
}
};
export const handler = middy(handlerCreateHuman)
// ... los demás middlewaresCapítulo 5: El Principio de la Pastilla Roja
Ya es conocido que en este blog nos tomamos el Principio de Responsabilidad Única muy en serio. Tener la lógica de autenticación directamente en el handler rompe completamente el SRP.
5.1: El Servicio del Oráculo
Movemos toda la lógica relacionada a la llamada de red del endpoint de autenticación a nuestro servicio:
export async function verifyToken(token: string) {
try {
const authResponse = await axios.get("https://api.matrix.fake/me", {
headers: { authorization: token }
});
return authResponse.data as User;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401)
return undefined;
// Otras validaciones...
}
}
Ahora nos aseguramos que si el token es válido retorne un usuario y si no, simplemente retorne undefined.
5.2: Actualizando el handler
En este paso solo nos toca importar nuestra función y eliminar la lógica del endpoint que habíamos realizado:
const handlerCreateHuman: Handler = async event => {
try {
const { authorization } = event.headers as Headers;
const user = await verifyToken(authorization);
if (!user) return formatJSONResponse(401, {
message: "Token inválido. No eres El Elegido."
}
);
const createHuman = event.body as Human;
const human = await humanService.create(createHuman);
return formatJSONResponse(201, human);
} catch (error) {
// Validaciones...
}
};
export const handler = middy(handlerCreateHuman)
.use(httpHeaderNormalizer())
.use(validateHeaders())
.use(validateBody())
.use(rateLimit());
Con esto, le quitamos peso al handler y delegamos la responsabilidad de verificar token a una función exclusiva.
Capítulo 6: El Middleware de la Verdad
El handler sigue sabiendo demasiado. Aunque hemos avanzado considerablemente, la validación del token no es su responsabilidad. Es hora de escalar al siguiente nivel y exprimir más el SRP:
6.1: Creando nuestro Auth Middleware
Comenzaremos creando un middlware simple que utilice la lógica del service ya creado:
export const auth = () => ({
before: async request => {
const token = request.event.headers.authorization;
const user = await verifyToken(token);
if (!user) return formatJSONResponse(401, {
message: "Token inválido. No eres El Elegido."
}
);
request.event.user = user; // Inyectamos el usuario al contexto
}
});
6.2: Handler Zen
Ahora nuestro handler es tan puro como el código de la Matrix:
const handlerCreateHuman: Handler = async event => {
try {
const createHuman = event.body as Human;
const human = await humanService.create(createHuman);
return formatJSONResponse(201, human);
} catch (error) {
// Validaciones...
}
};
export const handler = middy(handlerCreateHuman)
.use(httpHeaderNormalizer())
.use(validateHeaders())
.use(auth())
.use(validateBody())
.use(rateLimit());
Como se ve ahora en el código, eliminamos todo rastro de la lógica de autenticación en el handler, permitiendo que todo se haga vía middleware.
Capítulo 7: Los Permisos de la Matrix
Nuestro middleware ha alcanzado el nivel del arquitecto: no solo verifica identidades, sino que aplica las reglas de la Matrix con precisión milimétrica. Pese a esto ¿Qué pasaría si solo quiero que los usuarios con el cargo admin puedan acceder a este endpoint?
7.1: Modificar el middleware de auth
La idea es la siguiente: auth recibirá como parámetro la lista de roles que debe tener el endpoint. Si se cumplen, continuará correctamente; caso contrario lanzará un 403 indicando que el usuario no posee el rol necesario para entrar:
export const auth = (roles: string[] = []) => ({
before: async request => {
const token = request.event.headers.authorization;
const user = await verifyToken(token);
if (!user) return formatJSONResponse(401, {
message: "Token inválido. No eres El Elegido."
}
);
const hasRoles = roles.includes(user.role)
if (!hasRoles) return formatJSONResponse(403, {
message: "No tienes el rol necesario."
}
);
request.event.user = user;
}
});
7.2: Handler de Alto nivel
Por último, solo nos queda añadir la lista de roles que debe tener el endpoint:
export const handler = middy(handlerCreateHuman)
.use(httpHeaderNormalizer())
.use(validateHeaders())
.use(auth(["admin"])) // Solo permite usuarios con este rol
.use(validateBody())
.use(rateLimit());
Con este sistema, hemos creado un guardián implacable para nuestras APIs: un middleware que no solo valida identidades como un Agente Smith, sino que aplica permisos con la precisión del Arquitecto. Ahora cualquier handler puede volverse seguro con solo una línea:
.use(auth(['rol_requerido']))Capítulo 8: El elegido
Hemos comprobado que la verdadera seguridad en serverless opera en capas: primero el formato (Zod), luego la autenticación (verifyToken), y finalmente la autorización (roles). Cada una es como un anillo de defensa de Zion, donde el fallo de uno no compromete a los demás.
Recuerda: No es que el middleware sea perfecto... es que sigue las reglas de la Matrix al pie de la letra.
Referencias:





Member discussion