Volver
Cómo incrementar la seguridad de nuestra API REST optimizando el uso de las API KEYS
Categorías
API Rest | Backend | Cybersecurity
Fecha
19-08-2024
El uso de API KEYS es un método fundamental para proteger nuestra API REST, aunque con limitaciones. En este artículo exploramos cómo incrementar la seguridad mediante la introducción de una clave pública y una clave privada, junto con una firma generada con HMAC-SHA256. Además, se añaden complejidades como el método HTTP y el endpoint en la firma para limitar acciones en caso de comprometerse la misma. Con este enfoque podremos incrementar la seguridad de nuestra API REST con tan sólo unas pocas líneas de código.

El uso de API Keys es un método fundamental para proteger nuestra API REST, aunque con limitaciones. En este artículo exploramos cómo incrementar la seguridad mediante la introducción de una clave pública y una clave privada, junto con una firma generada con HMAC-SHA256. Además, se añaden complejidades como el método HTTP y el endpoint en la firma para limitar acciones en caso de comprometerse la misma. Con este enfoque podremos incrementar la seguridad de nuestra API REST con tan sólo unas pocas líneas de código.
Método básico para proteger una API REST utilizando API Keys
El uso de API Keys es uno de los métodos más básicos para agregar una capa de seguridad a nuestra API REST. Cada cliente o aplicación que quiera acceder a nuestra API recibe una clave única (API Key) que debe incluir en cada solicitud.
El proceso de este método se puede resumir en los siguientes pasos:
- El cliente envía la API Key junto con la solicitud a la API.
- El servidor valida el API Key.
- Si la API Key es válida, el servidor responde a la solicitud.
- Si la API Key no es válida, la solicitud es rechazada.
Aunque este método ofrece una capa de seguridad inicial, es importante notar que también tiene sus vulnerabilidades. Por ejemplo, si la API Key de un cliente es comprometida (robada), el atacante puede utilizarla para hacer cualquier cantidad de solicitudes a la API, hasta que no se invalide dicha API Key y se le proporcione otra nueva al cliente afectado. Este proceso puede ser tedioso y provocar periodos de inactividad o abuso de recursos de la API.
Por lo tanto, aunque es un método común y fácil de implementar, es crucial considerar medidas adicionales de seguridad para proteger nuestras API REST. A continuación, exploraremos algunas de estas medidas.
Mejorando la seguridad de nuestra API REST mediante la introducción de una segunda clave
Esta mejora se basa en entregar a cada uno de nuestros clientes un par de claves: una clave pública y una clave privada. La clave pública nos sirve para identificar al cliente, mientras que la clave privada le permite al cliente construir su clave de firma, que enviará junto a la solicitud a la API.
Haciendo uso de la clave privada el cliente debe crear una clave de firma a través del algoritmo SHA-256. El resultado de esta operación es lo que se conoce como un hash. En criptografía informática, un hash es un valor o una cadena de caracteres de longitud fija que se genera a partir de un conjunto de datos de entrada de longitud variable utilizando un determinado algoritmo. Una vez hecho esto debemos implementar en nuestra API REST un mecanismo que sea capaz de leer y validar dicha clave de firma. A continuación se muestra un ejemplo desarrollador en TypeScript de un middleware de Koa.
const publicAuthorization = async (ctx: RouterContext, next: () => Promise<any>) => {
  const publicKey: string = ctx.headers['x-api-key'] as string;
  const signatureKey: string = ctx.headers['x-signature-key'] as string;
  const secret: string = API_KEYS[publicKey].secret;
  const hash: string = crypto.createHmac('sha256', secret).digest('hex');
  if (hash !== signatureKey) return response.FORBIDDEN(ctx, 'SIGNATURE KEY not valid.');
  return await next();
};Este middleware recibe dos claves en las cabeceras de la solicitud: la clave pública y la clave de firma. El funcionamiento es sencillo, a través de la clave pública recogemos la correspondiente clave privada del cliente, ahora aplicamos el algoritmo hash a dicha clave privada y lo comparamos con la clave de firma que ha enviado el cliente. Si el hash generado no coincide con la clave de firma recibida, la solicitud se rechaza.
Sin embargo, con esta mejora realmente no hemos ganado mucha seguridad porque, si roban la clave de firma, la seguridad del sistema sigue siendo igual de vulnerable, habría que volver a generar una clave privada, por lo que estamos en el mismo caso que en el punto anterior. A estas alturas del artículo ya es bastante evidente cual es el objetivo de nuestro desarrollo: proteger al cliente y a nuestra API REST ante un robo de credenciales. En este artículo no vamos a profundizar cómo funciona HMAC con SHA-256 debido a que para ello necesitaríamos escribir otro artículo, sepamos tan sólo que es una herramienta más a nuestro alcance para incrementar la seguridad de nuestra API.
Salpimentando la clave de firma para construir un sistema más seguro
Es hora de ponernos el delantal y el gorro de chef. En la cocina, salpimentar al gusto las comidas es algo muy importante, ya que podemos darle un toque diferenciador a nuestros platos. Eso mismo es lo que vamos a realizar ahora con la clave de firma que utilizamos para validar la petición. Siguiendo con el símil, en la cocina podemos utilizar distintos elementos para aliñar los platos, como sal, pimienta blanca, pimienta negra… En este caso, vamos a utilizar también distintos elementos para ir modificando la clave de firma y hacerla cada vez más robusta.
El primer elemento que vamos a añadir es el tipo de petición HTTP que está realizando el usuario como elemento para salpimentar la clave de firma. De manera que si un atacante roba la clave de firma, solo podrá realizar peticiones del mismo tipo. Por ejemplo, si la clave de firma se generó sobre una petición GET, al atacante le sería imposible hacer una petición sobre un endpoint de tipo POST, en caso de que existiese. Pero, ¿cómo salpimentamos la clave? Para ello, utilizamos el método update en el caso de la librería crypto, como podemos ver a continuación:
const publicAuthorization = async (ctx: RouterContext, next: () => Promise<any>) => {
  const publicKey: string = ctx.headers['x-api-key'] as string;
  const signatureKey: string = ctx.headers['x-signature-key'] as string;
  const method: string = ctx.request.method;
  if (!API_KEYS[publicKey]) return response.UNAUTHORIZED(ctx, 'Not allowed.');
  const secret: string = API_KEYS[publicKey].secret;
  const message: string = `${method}`;
  const hash: string = crypto.createHmac('sha256', secret).update(message).digest('hex');
  if (hash !== signatureKey) return response.FORBIDDEN(ctx, 'SIGNATURE KEY not valid.');
  return await next();
};Ahora solo es necesario seguir añadiendo elementos que hagan más compleja la clave de firma y que nos permitan poner pequeños mecanismos de seguridad que reduzcan el daño ante un ataque. Por ejemplo, podemos utilizar el endpoint de la petición para lograr este objetivo, de manera que si la clave de firma es robada, solo podrán realizarse peticiones sobre ese mismo endpoint y no sobre otros:
const publicAuthorization = async (ctx: RouterContext, next: () => Promise<any>) => {
  const publicKey: string = ctx.headers['x-api-key'] as string;
  const signatureKey: string = ctx.headers['x-signature-key'] as string;
  const method: string = ctx.request.method;
  const endpoint: string = ctx.request.originalUrl;
  if (!publicKey || !signatureKey) return response.UNAUTHORIZED(ctx, 'Not allowed.');
  if (!API_KEYS[publicKey]) return response.UNAUTHORIZED(ctx, 'Not allowed.');
  const secret: string = API_KEYS[publicKey].secret;
  const message: string = `${method}|${endpoint}`;
  const hash: string = crypto.createHmac('sha256', secret).update(message).digest('hex');
  if (hash !== signatureKey) return response.FORBIDDEN(ctx, 'SIGNATURE KEY not valid.');
  return await next();
};De esta forma, si la clave de firma ha sido robada sobre el endpoint GET http://localhost:8080/brands/5/models, el atacante solo podrá realizar peticiones sobre ese endpoint y no sobre otros de nuestra API REST.
Ahora solo es cuestión de añadir otros elementos para incrementar la complejidad del mensaje con el que salpimentamos la clave privada. Podemos utilizar los distintos parámetros de la petición, como los de query o el propio body de la petición en caso de que exista. El resultado y objetivo de este proceso es minimizar el número y tipo de operaciones que puede realizar un atacante en caso de haber robado la clave de firma. En conclusión, es complicado alcanzar un sistema inquebrantable pero como desarrolladores si que podemos ir poniendo distintas trampas para que atacar nuestro sistema sea un poco más complicado con cada una de ellas.
Enjoy.