Enrutamiento dinámico basado en credenciales de usuario con API Gateway
April 9, 2023
Enrutamiento dinámico basado en reclamaciones JWT con Apache APISIX y Okta
El enrutamiento dinámico es una característica poderosa de la mayoría de las puertas de enlace de API modernas que permite enrutar solicitudes entrantes en tiempo real a diferentes servicios backend según varios criterios, como encabezados HTTP, parámetros de consulta o incluso el cuerpo de la solicitud.
Al aprovechar los complementos integrados existentes de Apache APISIX, los desarrolladores también pueden crear reglas de enrutamiento dinámico basadas en varias credenciales de usuario, como tokens de acceso, claves de API o ID de usuario. En este artículo, exploraremos los beneficios de adoptar el enrutamiento dinámico basado en atributos de autenticación con Apache APISIX y te mostraremos un ejemplo de configuración de cómo enrutar dinámicamente las solicitudes de los clientes a los servicios backend responsables basándose en la reclamación del token JWT.
Objetivos de aprendizaje
A lo largo del artículo aprenderás lo siguiente:
- Enrutar dinámicamente el tráfico con una puerta de enlace de API.
- ¿Por qué necesitamos el enrutamiento dinámico basado en credenciales de usuario?
- Enrutamiento dinámico basado en reclamaciones de tokens JWT con Apache APISIX.
Puerta de enlace de API: Enrutamiento dinámico de tráfico
El enrutamiento dinámico de tráfico con la puerta de enlace de API se puede utilizar en una amplia gama de aplicaciones y escenarios para optimizar el rendimiento, mejorar la seguridad y garantizar que los usuarios tengan acceso a los recursos adecuados.
Al enrutar dinámicamente el tráfico, un sistema puede equilibrar la carga entre diferentes servidores o servicios. Puede ayudar a garantizar la alta disponibilidad al enrutar el tráfico a servicios o servidores disponibles. Si un servicio o servidor falla, el tráfico puede redirigirse automáticamente a otro servicio o servidor disponible.
El enrutamiento dinámico también se puede utilizar para enrutar el tráfico basado en la geolocalización del usuario. Esto puede ayudar a garantizar que los usuarios se conecten al servidor o servicio más cercano, mejorando los tiempos de respuesta y reduciendo la latencia.
Puerta de enlace de API: Enrutamiento dinámico basado en la identidad del usuario
A menudo, queremos enrutar el tráfico a servicios específicos, rutas o mostrar solo datos relacionados con el usuario basados en la identidad proporcionada por el usuario. Por ejemplo, en aplicaciones multiinquilino, diferentes inquilinos pueden tener acceso a diferentes servicios o recursos. En este caso, la puerta de enlace de API puede enrutar el tráfico solo a los recursos del inquilino adecuado basándose en las credenciales del usuario. O en aplicaciones móviles, puede enrutar el tráfico a servicios específicos según el tipo de dispositivo o sistema operativo.
Uno de los enfoques comunes es utilizar tokens JWT para autenticar y autorizar solicitudes a las API. Esto significa que podemos crear reglas de enrutamiento complejas con la puerta de enlace de API que tengan en cuenta las reclamaciones presentes en el token JWT y utilicen esta información para decidir a dónde reenviar la solicitud o qué datos mostrar. Este enfoque es particularmente útil cuando tienes múltiples usuarios en el sistema que requieren diferentes niveles de control de acceso.
Demo: Enrutamiento dinámico basado en reclamaciones de tokens JWT
En esta demo, utilizamos la API backend pública llamada Conference API con información sobre sesiones, ponentes y temas de conferencias. En la realidad, este podría ser tu servicio backend. Supongamos que queremos filtrar y recuperar solo las sesiones pertenecientes a un ponente específico que ha iniciado sesión en el sistema utilizando sus credenciales, como un token JWT. Por ejemplo, https://conferenceapi.azurewebsites.net/speaker/1/sessions
La solicitud muestra solo las sesiones de un ponente con un ID único, y este ID único proviene de la reclamación del token JWT como parte de su carga útil. Observa la siguiente estructura de carga útil del token decodificado, donde se incluye un campo speakerId
:
En este escenario, enviamos solicitudes a la misma Ruta en la puerta de enlace de API, y esta calcula la URI dinámica a partir del encabezado de autorización y reenvía la solicitud a la URI (ver el diagrama a continuación para entender el flujo). Para hacerlo, vamos a implementar un enrutamiento dinámico en el nivel de la puerta de enlace de API Apache APISIX basado en la reclamación del token JWT mediante el uso de los siguientes complementos:
- El complemento openid-connect que interactúa con el proveedor de identidad (IdP) y puede interceptar solicitudes no autenticadas a tiempo para aplicaciones backend. Como proveedor de identidad, utilizamos Okta, que emite un token JWT con nuestra reclamación personalizada y valida el token JWT. O puedes usar otros IdP como Keycloak, Ory Hydra, o incluso puedes usar el complemento jwt para crear un token JWT, autenticar y autorizar solicitudes.
- El complemento serverless-pre-function para escribir un código de función Lua personalizado que intercepta la solicitud, decodifica, analiza una reclamación del token JWT y almacena el valor de la reclamación en un nuevo encabezado personalizado para tomar decisiones de autorización adicionales.
- El complemento proxy-rewrite, una vez que tenemos la reclamación en el encabezado, utilizamos este complemento como mecanismo de reenvío de solicitudes para determinar qué ruta URI debe usarse para recuperar sesiones específicas del ponente basándose en la variable de encabezado Nginx, en nuestro caso es
speakerId
que cambia dinámicamente para crear diferentes rutas/speaker/$http_speakerId/sessions
. El complemento reenviará la solicitud al recurso relacionado en la Conference API.
Una vez que entendemos lo que vamos a cubrir en la demo, revisemos los requisitos previos para comenzar a configurar el escenario anterior y completar el tutorial.
Requisitos previos
- Docker se utiliza para instalar etcd y APISIX en contenedores.
- curl se utiliza para enviar solicitudes a APISIX para configurar rutas, upstreams y configuraciones de complementos. También puedes usar herramientas fáciles como Postman para interactuar con la API.
- Apache APISIX está instalado en tu entorno objetivo. APISIX se puede instalar y comenzar fácilmente con la siguiente guía de inicio rápido.
- Asegúrate de que tu cuenta de OKTA esté creada, hayas registrado una nueva aplicación (puedes seguir esta guía Configuración de Okta), agregar una reclamación personalizada a un token usando el panel de Okta, y solicitar un token que contenga la reclamación personalizada llamada
speakerId
.
Configurar el servicio backend (upstream)
Necesitarás configurar el servicio backend para la Conference API al que deseas enrutar las solicitudes. Esto se puede hacer agregando un servidor upstream en Apache APISIX a través de la API de administración.
curl "http://127.0.0.1:9180/apisix/admin/upstreams/1" -X PUT -d '
{
"name": "Conferences API upstream",
"desc": "Register Conferences API as the upstream",
"type": "roundrobin",
"scheme": "https",
"nodes": {
"conferenceapi.azurewebsites.net:443": 1
}
}'
Crear una configuración de complemento
A continuación, configuramos un nuevo objeto de configuración de complemento. Utilizaremos 3 complementos: openid-connect, serverless-pre-function y proxy-rewrite respectivamente, como discutimos los casos de uso de cada complemento anteriormente. Solo necesitas reemplazar los atributos del complemento openid-connect
(ClienID, Secret, Discovery y Introspection endpoints) con tus propios detalles de Okta antes de ejecutar el comando curl.
curl "http://127.0.0.1:9180/apisix/admin/plugin_configs/1" -X PUT -d '
{
"plugins": {
"openid-connect":{
"client_id":"{YOUR_OKTA_CLIENT_ID}",
"client_secret":"{YOUR_OKTA_CLIENT_SECRET}",
"discovery":"https://{YOUR_OKTA_ISSUER}/oauth2/default/.well-known/openid-configuration",
"scope":"openid",
"bearer_only":true,
"realm":"master",
"introspection_endpoint_auth_method":"https://{YOUR_OKTA_ISSUER}/oauth2/v1/introspect",
"redirect_uri":"https://conferenceapi.azurewebsites.net/"
},
"proxy-rewrite": {
"uri": "/speaker/$http_speakerId/sessions",
"host":"conferenceapi.azurewebsites.net"
},
"serverless-pre-function": {
"phase": "rewrite",
"functions" : ["return function(conf, ctx)
-- Importar bibliotecas necesarias
local core = require(\"apisix.core\")
local jwt = require(\"resty.jwt\")
-- Recuperar el token JWT del encabezado Authorization
local jwt_token = core.request.header(ctx, \"Authorization\")
if jwt_token ~= nil then
-- Eliminar el prefijo Bearer del token JWT
local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
if jwt_token_only ~= nil then
-- Decodificar el token JWT
local jwt_obj = jwt:load_jwt(jwt_token_only)
if jwt_obj.valid then
-- Recuperar el valor de la reclamación speakerId del token JWT
local speakerId_claim_value = jwt_obj.payload.speakerId
-- Almacenar el valor de la reclamación speakerId en la variable de encabezado
core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
end
end
end
end
"]}
}
}'
En la configuración anterior, la parte más difícil de entender puede ser el código de función personalizado que escribimos en Lua dentro del complemento serverless-pre-function
:
return function(conf, ctx)
-- Importar bibliotecas necesarias
local core = require(\"apisix.core\")
local jwt = require(\"resty.jwt\")
-- Recuperar el token JWT del encabezado Authorization
local jwt_token = core.request.header(ctx, \"Authorization\")
if jwt_token ~= nil then
-- Eliminar el prefijo Bearer del token JWT
local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
if jwt_token_only ~= nil then
-- Decodificar el token JWT
local jwt_obj = jwt:load_jwt(jwt_token_only)
if jwt_obj.valid then
-- Recuperar el valor de la reclamación speakerId del token JWT
local speakerId_claim_value = jwt_obj.payload.speakerId
-- Almacenar el valor de la reclamación speakerId en la variable de encabezado
core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
end
end
end
end
Básicamente, este complemento se ejecutará antes que los otros dos complementos y hace lo siguiente:
- Recupera el token JWT del encabezado Authorization.
- Elimina el prefijo "Bearer " del token JWT.
- Decodifica el token JWT utilizando la biblioteca resty.jwt.
- Recupera el valor de la reclamación "speakerId" del token JWT decodificado.
- Finalmente, almacena el valor de la reclamación "speakerId" en la variable de encabezado speakerId.
Configurar una nueva Ruta
Este paso implica configurar una nueva ruta que utilice la configuración del complemento y configurar la ruta para que funcione con el upstream (haciendo referencia a sus ID) que creamos en los pasos anteriores:
curl "http://127.0.0.1:9180/apisix/admin/routes/1" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"name":"Conferences API speaker sessions route",
"desc":"Create a new route in APISIX for the Conferences API speaker sessions",
"methods": ["GET"],
"uri": "/sessions",
"upstream_id":"1",
"plugin_config_id":1
}'
En la configuración anterior, definimos las reglas de coincidencia de ruta, como que solo las solicitudes HTTP GET a la URI /sessions
serán enrutadas al servicio backend correcto.
Obtener un token de Okta
Después de configurar el upstream, los complementos y la ruta en el lado de APISIX, ahora solicitamos un token de Okta que contenga nuestra reclamación personalizada speakerId
. Puedes seguir la guía que incluye información sobre cómo construir una URL para solicitar un token con Okta o simplemente usar la URL resultante a continuación con tu emisor de Okta y ID de cliente:
https://{YOUR_OKTA_ISSUER}/oauth2/default/v1/authorize?client_id={YOUR_OKTA_CLIENT_ID}
&response_type=id_token
&scope=openid
&redirect_uri=https%3A%2F%2Fconferenceapi.azurewebsites.net
&state=myState
&nonce=myNonceValue
Después de pegar la solicitud en tu navegador, el navegador se redirige a la página de inicio de sesión de Okta y genera un Token de ID.
https://conferenceapi.azurewebsites.net/#id_token={TOKEN_WILL_BE_HERE}
Ten en cuenta que el proceso para recuperar un token puede ser diferente con otros proveedores de identidad.
Para verificar el token de ID devuelto, puedes copiar el valor y pegarlo en cualquier decodificador JWT (por ejemplo, https://token.dev).
Probar el enrutamiento dinámico
Finalmente, ahora podemos verificar que la solicitud se está enrutando a la ruta URI correcta (con sesiones específicas del ponente) basándose en los criterios de coincidencia y la reclamación del token JWT ejecutando otro simple comando curl:
curl -i -X "GET [http://127.0.0.1:9080/sessions](http://127.0.0.1:9080/sessions)" -H "Authorization: Bearer {YOUR_OKTA_JWT_TOKEN}"
Aquí vamos, el resultado es el esperado. Si configuramos speakerId a 1 en la reclamación del token JWT de Okta, Apisix enrutó la solicitud a la ruta URI relevante y devolvió todas las sesiones de este ponente en la respuesta.
{
"collection": {
"version": "1.0",
"links": [],
"items": [
{
"href": "https://conferenceapi.azurewebsites.net/session/114",
"data": [
{
"name": "Title",
"value": "\r\n\t\t\tIntroduction to Windows Azure Part I\r\n\t\t"
},
{
"name": "Timeslot",
"value": "04 December 2013 13:40 - 14:40"
},
{
"name": "Speaker",
"value": "Scott Guthrie"
}
],
"links": [
{
"rel": "http://tavis.net/rels/speaker",
"href": "https://conferenceapi.azurewebsites.net/speaker/1"
},
{
"rel": "http://tavis.net/rels/topics",
"href": "https://conferenceapi.azurewebsites.net/session/114/topics"
}
]
},
{
"href": "https://conferenceapi.azurewebsites.net/session/121",
"data": [
{
"name": "Title",
"value": "\r\n\t\t\tIntroduction to Windows Azure Part II\r\n\t\t"
},
{
"name": "Timeslot",
"value": "04 December 2013 15:00 - 16:00"
},
{
"name": "Speaker",
"value": "Scott Guthrie"
}
],
}
]
}
}
Conclusiones
- Con una puerta de enlace de API, puedes enrutar el tráfico a diferentes servicios backend basados en varios criterios.
- El enrutamiento dinámico se puede lograr dependiendo de los atributos del usuario especificados en el encabezado, consulta o cuerpo de la solicitud.
- Puedes crear reglas de enrutamiento complejas que tengan en cuenta las reclamaciones presentes en el token JWT y asegurar que solo las solicitudes autorizadas puedan acceder a tu API.