Confianza distribuida con autenticación entre servicios
En esta página
En una arquitectura orientada a servicios (SOA), los “servicios” son los bloques fundamentales de tu aplicación. Los servicios son autónomos, están débilmente acoplados y suelen ser stateless. Son autónomos porque no dependen del estado ni del contexto de otros servicios. Están débilmente acoplados porque no dependen de las tecnologías que usan los demás. Son stateless porque no guardan estado entre peticiones.
Los servicios son los bloques de tu aplicación y también los bloques de tu arquitectura de seguridad.
Distribuir la confianza
En una SOA, los servicios son independientes entre sí. Eso significa que también son independientes de los mecanismos de seguridad que usan los demás.
Eso es bueno.
Puedes usar mecanismos de seguridad distintos para servicios distintos. Distintos mecanismos de autenticación, de autorización, de cifrado, etc.
Y también es malo.
Tienes que gestionar mecanismos de seguridad distintos para servicios distintos. Distintos mecanismos de autenticación, de autorización, de cifrado, etc.
Autenticación entre servicios
En una SOA, los servicios son independientes entre sí. Ya ves por dónde va la cosa.
Autenticar las peticiones entre servicios es un reto. No puedes usar los mismos mecanismos que usas para autenticar usuarios. No puedes usar cookies, no puedes usar sesiones y, sobre todo, no quieres depender de un proveedor de identidad (IdP).
Con ofertas de plataforma como servicio (PaaS) como Cloudflare Workers, Heroku, Cloud Foundry y Vercel, ni siquiera puedes contar con que la infraestructura te dé un canal seguro entre servicios.
Necesitas una forma de autenticar las peticiones entre servicios que no dependa de la infraestructura ni de los mecanismos de seguridad de los demás servicios.
Ahí entran los JWT y los JWKS.
JSON Web Tokens (JWT)
Los JSON Web Tokens (JWT) son un formato compacto, seguro para URL, con el que dos partes se transfieren claims. Los claims de un JWT se codifican como un objeto JSON que sirve de payload de una estructura JSON Web Signature (JWS) o de texto plano de una estructura JSON Web Encryption (JWE). Así los claims pueden ir firmados o protegidos en su integridad con un código de autenticación de mensaje (MAC), o cifrados, o ambas cosas.
Estructura de un JWT
Un JWT es una cadena de tres partes: la cabecera, el payload y la firma. La cabecera y el payload son objetos JSON. La firma es una cadena codificada en Base64.
<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>
Cabecera del JWT
La cabecera suele tener dos partes: el tipo de token, que es JWT, y el algoritmo de firma, como HMAC SHA256 o RSA.
{
"alg": "HS256",
"typ": "JWT"
}
Payload del JWT
El payload contiene los claims. Los claims son afirmaciones sobre una entidad (normalmente el usuario) y metadatos adicionales. Hay tres tipos de claims: registrados, públicos y privados.
- Claims registrados: un conjunto de claims predefinidos, que no son obligatorios pero sí recomendables, para tener claims útiles e interoperables. Algunos son: iss (emisor), exp (caducidad), sub (sujeto), aud (audiencia) y otros.
- Claims públicos: los define a voluntad quien use los JWT. Para evitar colisiones, deberían definirse en el registro IANA de JSON Web Token o como una URI que contenga un espacio de nombres resistente a colisiones.
- Claims privados: claims a medida que sirven para compartir información entre partes que han acordado usarlos.
{
"sub": "1234567890",
"iss": "https://my-experience-api.willhackett.xyz",
"aud": "https://my-resource-api.willhackett.xyz"
}
Los claims iss y aud identifican al emisor y a la audiencia del JWT, respectivamente. El claim sub identifica al sujeto del JWT. Aquí, el sujeto es el ID del usuario en cuyo nombre se hace la petición. En un caso real, te recomiendo reempaquetar en el nuevo JWT el JWT con el que el usuario hizo la petición. Así el servicio receptor puede verificar la identidad del usuario y comprobar que tiene permisos para hacer la acción pedida en tu capa de recursos.
El emisor y la audiencia importarán más adelante, cuando hable de los límites de confianza.
Firma del JWT
Para crear la firma tienes que tomar la cabecera codificada, el payload codificado, un secreto y el algoritmo indicado en la cabecera, y firmar con todo eso.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
La firma sirve para verificar que el mensaje no se ha modificado por el camino. Si el token va firmado con una clave privada, también verifica que quien envía el JWT es quien dice ser.
JSON Web Key Sets (JWKS)
Un JSON Web Key Set (JWKS) es un conjunto de claves que contiene las claves públicas con las que se verifica cualquier JSON Web Token (JWT) emitido por el servidor de autorización y firmado con el algoritmo RS256.
Estructura de un JWKS
Un JWKS es un objeto JSON que representa un conjunto de JWK. El objeto JSON DEBE tener un miembro “keys”, que es un array de JWK. Este es el formato JWK Set.
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use": "enc",
"kid": "1"
},
{
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFb.....<remainder of RSA key omitted>.....ZU3e_TURo9-F1bp-hqOY_BLPbEx-Rmc6XaXk",
"e": "AQAB",
"alg": "RS256",
"kid": "2011-04-29"
}
]
}
En nuestro caso, solo nos interesan las claves públicas. Las claves públicas sirven para verificar la firma de los JWT.
Modelo de confianza
El modelo de confianza es simple. Se confía en que el servicio que emite el JWT emite JWT válidos. Se confía en que el servicio que recibe el JWT lo verifica. No se confía en que el servicio que recibe el JWT emita JWT válidos en nombre del servicio que llama.
Si el servicio A emite un JWT para el servicio B, se confía en que el servicio B lo verifica contra el JWKS que publica el servicio A. El servicio B no puede firmar un JWT y esperar que el servicio C se fíe de él. Aquí entran los límites de confianza.
Límites de confianza
Los límites de confianza son las fronteras entre servicios que se fían unos de otros.
Si el servicio A emite un JWT para el servicio B, se confía en que el servicio B lo verifica contra el JWKS que publica el servicio A.
Cada JWT lleva un claim iss y un claim aud. aud es la audiencia prevista del JWT y sirve para validar que la petición va al servicio correcto. iss es el emisor del JWT y sirve para validar que lo emitió un servicio de confianza.
El servicio B confía en que el servicio A emite JWT válidos. Supongamos que el hostname del servicio A es service-a.willhackett.xyz y el del servicio B es service-b.willhackett.xyz. El claim del emisor sería https://service-a.willhackett.xyz y el de la audiencia sería https://service-b.willhackett.xyz.
El servicio B aceptaría los tokens con el claim de emisor https://service-a.willhackett.xyz y el claim de audiencia https://service-b.willhackett.xyz. El servicio B llamaría al endpoint JWKS del servicio A para obtener las claves públicas con las que verificar los JWT que emite el servicio A.
Service mesh
Conviene preguntarse por qué no usar un service mesh. Los service mesh son estupendos, pero no son una bala de plata. Por desgracia, no encajan en todos los casos. Cuanto más desarrollar para la web pasa a ser desarrollar para el edge o sobre una plataforma, menos útiles resultan.
Dentro de tu infraestructura, un service mesh es una gran solución. Desplegar contenedores con un proxy sidecar es una forma fantástica de estandarizar la comunicación entre servicios. ¿Pero qué pasa cuando despliegas en una plataforma como Cloudflare Workers, Heroku, Cloud Foundry o Vercel?
Con las plataformas, tienes que apoyarte en estándares.
Otras ideas
Con este modelo de confianza distribuida, tendrías que asegurarte de que las claves RSA que genera tu runtime sean coherentes al escalar horizontalmente. Así, la llamada al JWKS siempre devolvería las mismas claves públicas.
Este es solo un estándar para autenticar servicio a servicio por HTTP. También podrías usar TLS mutuo (mTLS), secretos compartidos, etc. Lo que importa es que tengas un estándar que no dependa de la infraestructura ni de los mecanismos de seguridad de los demás servicios.