sábado, 8 de mayo de 2021

Triangulación para autenticar APIs públicas

 

Hace algún tiempo, se me presentó un nuevo proyecto. Tenía que diseñar la solución.

Entre otras cosas de las que hablaré más adelante, uno de los desafíos consistía en comunicar un conjunto de servicios que estaban expuestos a la internet pública.

Aunque no me sentía cómodo con eso, era uno de los requisitos; una aplicación personalizada debía ser capaz de hablar con la API directamente, mientras que la mayoría de las aplicaciones hablarían con una API específica (punto de entrada o EP, por Entry Point).

Este EP tendría que hablar con otros servicios también.

Entonces, el problema era: ¿Cómo controlo que cada solicitud entre dos APIs esté realmente autorizada para pedir y leer información?

Todos los métodos de autenticación que había usado hasta ese momento seguían este esquema:

  1. Se me proporcionaba un par de valores clave para enviar en los encabezados (headers) cada vez.

  2. Tenía que solicitar un nuevo token cada vez que quería enviar un mensaje.

Pero esta vez quería hacer algo diferente y poner en práctica un consejo que me dio un colega con más experiencia: los triángulos tienden a ser seguros.

El tercero en discordia:

Para armar mi triángulo, creé una nueva API que iba a ser la "fuente de verdad" (source of truth) para los permisos. Llamémosla SoT.

Entonces, el algoritmo general era:

  1. Cada API tendrá su propia clave secreta.

  2. SoT tendrá las claves secretas de todos.

  3. Cuando llegue una solicitud desde el EP, se creará una transacción en el SoT.

  4. Cada solicitud será firmada por el emisor con su secreto y la transacción.

  5. El SoT será el único capaz de decir si una firma es correcta o no.

  6. Cada vez que una API reciba una solicitud, le preguntará al SoT si la firma de la solicitud es válida.

Implementación:

De hecho, programar esa solución fue bastante sencillo; necesité dos componentes de cliente y los controladores del SoT:

Desde el EP, solicitar crear y firmar una transacción:

idTransaccion = nuevo uuid()

firma = nuevo Hash(EP_CLAVE_SECRETA, idTransaccion)

// EntryPoint como nombre de la API solicitante para el campo del firmante

ok = sot.crear('EntryPoint', idTransaccion, firma)

En el SoT:

claveRemota = claves.obtener(input.firmante)

si input.firma == nuevo Hash(claveRemota, input.idTransaccion)

crearTransaccion()

sino

Error(403, 'No se puede crear la transacción')

Desde una API solicitada (como middleware del endpoint):

si !sot.validar(input.firma, input.idTransaccion, input.firmante)

Error(403, 'Firma inválida')

continuar()

Una vez que una solicitud era autenticada, otras llamadas internas podían autenticarse de la misma manera.

Sobre los secretos: deberían generarse aleatoriamente al momento del despliegue (deploy). Cada vez que desplegamos una API, renovamos su secreto y le informamos al SoT sobre el cambio. Se almacenan en variables de entorno.

Riesgos:

No encontré muchos motivos de preocupación. Mientras mantengamos los secretos realmente en secreto, no debería haber problemas.

Quien quiera realizar solicitudes no autorizadas tendría que conocer los secretos y la función de hash implementada.

Resumen:

Es bastante fácil implementar autenticaciones en triángulo por nuestra cuenta con cierto nivel de seguridad.

Lo que debemos cuidar es mantener las llaves en secreto y no exponer nuestras funciones de hash.

Saludos:

Gracias por leer. Cualquier sugerencia o comentario es bienvenido.


Productividad Ilusoria: ¿Optimización real o desplazamiento de carga cognitiva?

  Siguiendo con una serie de reflexiones sobre el escenario actual del desarrollo de software, comparto una de las ideas más controversiales...