Autenticación de clientes de API con JWT y NGINX Plus

NGINX | 02 de diciembre de 2021

Los tokens web JSON (JWT, pronunciado «yots») son un medio compacto y muy portátil para intercambiar información de identidad. La especificación JWT ha sido una base importante de OpenID Connect, ya que proporciona un token de inicio de sesión único para el ecosistema OAuth 2.0. Los JWT también pueden utilizarse como credenciales de autenticación por sí mismos y constituyen una forma mejor de controlar el acceso a las API web que las claves de API tradicionales.

NGINX Plus R10 y las versiones posteriores pueden validar JWT directamente. En esta entrada del blog describimos cómo utilizar NGINX Plus como puerta de enlace de API, proporcionando un frontend para un endpoint de API y utilizando JWT para autenticar las aplicaciones cliente.

La compatibilidad nativa con JWT solo está disponible en NGINX Plus, no en NGINX Open Source.

Editor – Esta entrada de blog se actualizó en diciembre de 2021 para utilizar la directiva auth_jwt_require introducida en NGINX Plus R25. Para obtener información detallada sobre la directiva, consulte Reglas personalizadas de validación de JWT en la entrada de blog en la que se anuncia NGINX Plus R25.

NGINX Plus R15 y las versiones posteriores también pueden controlar el «flujo de código de autorización» de OpenID Connect 1.0, lo que permite la integración con la mayoría de los principales proveedores de identidad. Para obtener más información, consulte Presentación de NGINX Plus R15<.htmlspan>.

Anatomía de un JWT

Los JWT constan de tres partes: una cabecera, una carga útil y una firma. Cuando se transmiten, tienen el siguiente aspecto. Hemos añadido saltos de línea para facilitar la lectura (el JWT real es una única cadena) y un código de colores para distinguir las tres partes:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICAgInN1YiI6ICJsYzEiLAogICAgImVtYWlsIjogImxpYW0uY3JpbGx5QG5naW54LmNvbSIsCn0=.VGYHWPterIaLjRi0LywgN3jnDUQbSsFptUw99g2slfc

Como se muestra, un punto ( . ) separa el encabezado, la carga útil y la firma. El encabezado y la carga útil son objetos JSON codificados en Base64. La firma se cifra mediante el algoritmo especificado en el encabezado alg, como podemos comprobar al descodificar nuestro JWT de ejemplo:

Codificado DecodificadoEncabezado eyJhbGciOiJIUzI1NiIsInR5cCI6Ik pXVCJ9 {
    "alg": "HS256",
    "typ": "JWT"
}
Carga útil ewogICAgInN1YiI6ICJsYzEiLAogICAgImVtYWlsIjogImxpYW0uY3JpbGx5QG5naW54LmNvbSIsCn0= {
    "sub": "lc1",
    "email": "liam.crilly@nginx.com",
}

El estándar JWT define varios algoritmos de firma. El valor HS256 de nuestro ejemplo hace referencia a HMAC SHA‑256, que utilizamos para todos los JWT de ejemplo de esta entrada del blog. NGINX Plus admite los algoritmos de firma HSxxx, RSxxx y ESxxx signature algorithms definidos en el estándar. La posibilidad de firmar criptográficamente los JWT hace que sean ideales para utilizarlos como credenciales de autenticación.

JWT como clave de API

Una forma habitual de autenticar a un cliente de API (el cliente de software remoto que solicita recursos de la API) consiste en utilizar un secreto compartido, generalmente denominado clave de API. Una clave de API tradicional es, en esencia, una contraseña larga y compleja que el cliente envía como una cabecera HTTP adicional en todas y cada una de las solicitudes. El endpoint de la API permite acceder al recurso solicitado si la clave de API proporcionada figura en la lista de claves válidas. Por lo general, el propio endpoint de la API no valida las claves de API; en su lugar, una puerta de enlace de API se encarga del proceso de autenticación y dirige cada solicitud al endpoint correspondiente. Además de descargar el procesamiento, esto aporta las ventajas propias de un proxy inverso, como la alta disponibilidad y el equilibrio de carga entre varios endpoints de API.

Cliente de API y autenticación mediante JWT con una clave de API tradicional
La puerta de enlace de la API valida la clave de API consultando un registro de claves antes de enviar la solicitud
al endpoint de la API

Es habitual aplicar distintos controles de acceso y políticas a diferentes clientes de API. Con las claves de API tradicionales, es necesario realizar una consulta para asociar la clave de API a un conjunto de atributos. Realizar esta consulta en todas y cada una de las solicitudes tiene un impacto comprensible en la latencia general del sistema. Con JWT, estos atributos están integrados, lo que elimina la necesidad de realizar una consulta independiente.

El uso de JWT como clave de API ofrece una alternativa de alto rendimiento a las claves de API tradicionales, ya que combina una tecnología de autenticación basada en las mejores prácticas con un esquema basado en estándares para el intercambio de atributos de identidad.

Cliente de API y autenticación mediante JWT con JWT y NGINX Plus
NGINX Plus valida el JWT antes de pasar la solicitud a los endpoints de la API

Configuración de NGINX Plus como puerta de enlace de API con autenticación

La configuración de NGINX Plus para validar JWT es muy sencilla.

upstream api_server {
    server 10.0.0.1;
    server 10.0.0.2;
}

server {
    listen 80;

    location /products/ {
        auth_jwt "Products API";
        auth_jwt_key_file conf/api_secret.jwk;
        proxy_pass http://api_server;
    }
}

Lo primero que hacemos es especificar las direcciones de los servidores que alojan el endpoint de la API en el bloque upstream. El bloque location especifica que se debe autenticar cualquier solicitud a una URL que comience por /products/. La directiva auth_jwt define el dominio de autenticación que se devolverá (junto con un código de estado 401) si la autenticación no se realiza correctamente.

La directiva auth_jwt_key_file indica a NGINX Plus cómo validar el elemento de firma del JWT. En este ejemplo, utilizamos el algoritmo HMAC SHA‑256 para firmar los JWT, por lo que debemos crear una clave web JSON en conf/api_secret.jwk que contenga la clave simétrica utilizada para la firma. El archivo debe seguir el formato descrito en la especificación de claves web JSON; nuestro ejemplo tiene este aspecto:

{"keys":
    [{
        "k":"ZmFudGFzdGljand0",
        "kty":"oct",
        "kid":"0001"
    }]
}

La clave simétrica se define en el campo k, y este es el valor codificado en Base64URL de la cadena de caracteres de texto sin formato fantasticjwt. Obtuvimos el valor codificado ejecutando este comando:

$ echo -n fantasticjwt | base64 | tr '+/' '-_' | tr -d '='

El campo kty define el tipo de clave como una clave simétrica (secuencia de octetos). Por último, el campo kid (ID de clave) define un número de serie para esta clave web JSON, en este caso 0001, lo que nos permite admitir varias claves en el mismo archivo (especificado mediante la directiva auth_jwt_key_file) y gestionar el ciclo de vida de dichas claves y de los JWT firmados con ellas.

Ahora estamos listos para emitir JWT para nuestros clientes de API.

Emisión de un JWT para clientes de API

Como cliente de API de ejemplo, utilizaremos una aplicación de «sistema de presupuestos» y crearemos un JWT para dicho cliente. Primero, definimos la cabecera del JWT:

{
  "typ":"JWT",
  "alg":"HS256",
  "kid":"0001"
}

El campo typ define el tipo como token web JSON, el campo alg especifica que el JWT está firmado con el algoritmo HMAC SHA256 y el campo kid especifica que el JWT está firmado con la clave web JSON que tiene ese número de serie.

A continuación, definimos la carga útil del JWT:

{
  "name":"Quotation System",
  "sub":"quotes",
  "iss":"My API Gateway"
}

El campo sub (sujeto) es nuestro identificador único para el valor completo del campo name. El campo iss describe al emisor del JWT, lo que resulta útil si tu puerta de enlace de API también acepta JWT de emisores externos o de un sistema centralizado de gestión de identidades.

Ahora que tenemos todo lo necesario para crear el JWT, seguimos estos pasos para codificarlo y firmarlo correctamente. Los comandos y los valores codificados se muestran en varias líneas únicamente para facilitar su lectura; en realidad, cada uno se escribe o aparece en una sola línea.

  1. Aplane por separado el encabezado y la carga útil, y codifíquelos en Base64URL. 
$ echo -n '{"typ":"JWT","alg":"HS256","kid":"0001"}' | base64 | tr '+/' '-_' | tr -d '='eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ

$ echo -n '{"name":"Quotation System","sub":"quotes","iss":"My API Gateway"}' | base64 | tr '+/' '-_' | tr -d '='
eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsImlzcyI6Ik
15IEFQSSBHYXRld2F5In0
  1. Concatena el encabezado y la carga útil codificados con un punto (.) y asigna el resultado a la variable HEADER_PAYLOAD.
$ HEADER_PAYLOAD=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsIm lzcyI6Ik15IEFQSSBHYXRld2F5In0
  1. Firma el encabezado y la carga útil con nuestra clave simétrica y codifica la firma en Base64URL.
$ echo -n $HEADER_PAYLOAD | openssl dgst -binary -sha256 -hmac fantasticjwt | base64 | tr '+/' '-_' | tr -d '='ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I
  1. Añade la firma codificada al encabezado y a la carga útil.
$ echo $HEADER_PAYLOAD.ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I > quotes.jwt
  1. Realiza una prueba enviando una solicitud autenticada a la puerta de enlace de la API (en este ejemplo, la puerta de enlace se ejecuta en localhost).
$ curl -H "Authorization: Bearer `cat quotes.jwt`" http://localhost/products/widget1

El comando curl del paso 5 envía el JWT a NGINX Plus en forma de token de portador, que es el formato que NGINX Plus espera de manera predeterminada. NGINX Plus también puede obtener el JWT de una cookie o de un parámetro de la cadena de consulta; para configurarlo, incluya el parámetro token= en la directiva auth_jwt. Por ejemplo, con la siguiente configuración, NGINX Plus puede validar el JWT enviado mediante este comando curl:

$ curl http://localhost/products/widget1?apijwt=`cat quotes.jwt`
server {
    listen 80;

    location /products/ {
        auth_jwt "Products API" token=$arg_apijwt;
        auth_jwt_key_file conf/api_secret.jwk;
        proxy_pass http://api_server;
    }
}

Una vez que haya configurado NGINX Plus y generado y verificado un JWT como se muestra arriba, podrá enviar el JWT al desarrollador del cliente de la API y acordar el mecanismo que se utilizará para incluirlo en cada solicitud a la API.

Aprovechamiento de las declaraciones JWT para el registro y la limitación de solicitudes

Una de las principales ventajas de los JWT como credenciales de autenticación es que transmiten «declaraciones», que representan entidades asociadas al JWT y a su carga útil (por ejemplo, su emisor, el usuario para el que se emitió y el destinatario previsto). Tras validar el JWT, NGINX Plus puede acceder como variables a todos los campos presentes en la cabecera y la carga útil. Para acceder a ellos, se antepone $jwt_header_ o $jwt_claim_ al campo deseado (por ejemplo, $jwt_claim_sub para la declaración sub). Esto significa que podemos enviar muy fácilmente la información contenida en el JWT al endpoint de la API sin necesidad de implementar el procesamiento de JWT en la propia API.

Este ejemplo de configuración muestra algunas de las funcionalidades avanzadas.

log_format jwt '$remote_addr - $remote_user [$time_local] "$request" '
               '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
               '$jwt_header_alg $jwt_claim_sub';

limit_req_zone $jwt_claim_sub zone=10rps_per_client:1m rate=10r/s;

server {
    listen 80;

    location /products/ {
        auth_jwt "Products API";
        auth_jwt_key_file conf/api_secret.jwk;

        limit_req zone=10rps_per_client;

        proxy_pass http://api_server;
        proxy_set_header API-Client $jwt_claim_sub;

        access_log /var/log/nginx/access_jwt.log jwt;
    }
}

La directiva log_format define un nuevo formato denominado jwt, que amplía el formato de registro común con dos campos adicionales: $jwt_header_alg y $jwt_claim_sub. Dentro del bloque location, utilizamos la directiva access_log para escribir registros con los valores obtenidos del JWT validado.

En este ejemplo, también utilizamos variables basadas en notificaciones para limitar la tasa de solicitudes de la API por cliente de API, en lugar de por dirección IP. Esto resulta especialmente útil cuando varios clientes de API están integrados en un único portal y no se pueden diferenciar por su dirección IP. La directiva limit_req_zone utiliza la notificación sub del JWT como clave para calcular los límites de tasa, que posteriormente se aplican al bloque location mediante la inclusión de la directiva limit_req.

Por último, proporcionamos el sujeto del JWT como una nueva cabecera HTTP cuando la solicitud se reenvía mediante proxy al endpoint de la API. La directiva proxy_set_header añade una cabecera HTTP denominada API‑Client, que el endpoint de la API puede procesar fácilmente. Por tanto, el endpoint de la API no necesita implementar ninguna lógica de procesamiento de JWT. Esto resulta cada vez más útil a medida que aumenta el número de endpoints de la API.

Revocación de JWT

En ocasiones, puede ser necesario revocar o volver a emitir el JWT de un cliente de API. Al combinar un bloque map sencillo con la directiva auth_jwt_require, podemos denegar el acceso a un cliente de API marcando su JWT como no válido hasta que se alcance la fecha de caducidad del JWT (representada en la declaración exp), momento en el que se puede eliminar de forma segura la entrada de map correspondiente a ese JWT.

En este ejemplo, establecemos la variable $jwt_status en 0 o 1 según el valor de la declaración sub del token (recogido en la variable $jwt_claim_sub). A continuación, usamos la directiva auth_jwt_require en el bloque location para validar (o rechazar) adicionalmente el token. Para que sea válido, la variable $jwt_status no debe estar vacía ni ser igual a 0 (cero).

map $jwt_claim_sub $jwt_status { "quotes" 0; "test" 0; default 1;
}

server {
    listen 80;

    location /products/ {
        auth_jwt "Products API";
        auth_jwt_key_file conf/api_secret.jwk;
        auth_jwt_require $jwt_status;

        proxy_pass http://api_server;
    }
}

Resumen

Los tokens web JSON son muy adecuados para proporcionar acceso autenticado a las API. Para los desarrolladores de clientes de API, son tan fáciles de gestionar como las claves de API tradicionales y proporcionan a la puerta de enlace de API información de identidad que, de otro modo, requeriría una consulta a una base de datos. NGINX Plus admite la autenticación mediante JWT y ofrece soluciones de configuración avanzadas basadas en la información contenida en el propio JWT. En combinación con otras funcionalidades de puerta de enlace de API, NGINX Plus permite ofrecer servicios basados en API con rapidez, fiabilidad, escalabilidad y seguridad.

Para probar JWT con NGINX Plus, inicie hoy mismo su prueba gratuita de 30 días o póngase en contacto con nosotros para hablar sobre sus casos de uso.


Compartir
Etiquetas: 2021, F5 NGINX, Tech

Sobre el autor

Liam Crilly
Liam CrillyDirector sénior de Gestión de Producto

Más artículos de Liam Crilly

Artículos relacionados del blog

Únete a F5 NGINX en AppWorld 2024
NGINX | 04/01/2024

Únete a F5 NGINX en AppWorld 2024

Descubre las numerosas razones para asistir al evento insignia de F5, que incluirá un emocionante anuncio sobre la cartera de productos NGINX.

Rotación de certificados SSL/TLS sin reinicios en NGINX Open Source
NGINX | 26/09/2023

Rotación de certificados SSL/TLS sin reinicios en NGINX Open Source

La función de diccionario compartido permite configurar NGINX sin necesidad de reiniciarlo al renovar los certificados SSL/TLS.

Exposición de APIs en Kubernetes
NGINX | 10/03/2022

Exposición de APIs en Kubernetes

La unidad 2 de Microservices March 2022 se centra en cómo exponer tus API en Kubernetes. Aquí tienes enlaces a los recursos que necesitas: la retransmisión en directo del 14 de marzo, artículos de blog y otros recursos de preparación, y el laboratorio práctico «Protege las API de Kubernetes mediante la limitación de solicitudes».

NGINX Sprint 2.0: visión clara, código renovado y nuevos compromisos con el código abierto
NGINX | 23/08/2021

NGINX Sprint 2.0: visión clara, código renovado y nuevos compromisos con el código abierto

Hoy, en NGINX Sprint 2.0, anunciamos cómo respaldaremos nuestra visión del futuro de las aplicaciones modernas con una mayor presencia en la comunidad de código abierto, incluida la participación en el proyecto comunitario Kubernetes, nuevos proyectos de software de código abierto y la arquitectura de referencia para aplicaciones modernas.

Presentamos NGINX Plus R22
NGINX | 09/06/2020

Presentamos NGINX Plus R22

NGINX Plus R22 incorpora nuevas funcionalidades, como la compatibilidad con OCSP para verificar la revocación de certificados en TLS mutuo, el uso de varios proveedores de identidad OIDC en una configuración, el seguimiento en tiempo real de la limitación de solicitudes y conexiones en el panel de NGINX Plus y mejoras en NGINX JavaScript.

Presentamos NGINX Plus R17
NGINX | 11/12/2018

Presentamos NGINX Plus R17

NGINX Plus R17 incorpora compatibilidad con la limitación de velocidad en dos fases y con TLS 1.3, la versión más reciente del protocolo Transport Layer Security. La configuración de OpenID Connect es más sencilla y NGINX ModSecurity WAF es el doble de rápido que antes. También se ha actualizado el módulo JavaScript de NGINX.