Autenticando clientes de API com JWT e NGINX Plus

NGINX | 02 de dezembro de 2021

Os JSON Web Tokens (JWTs, pronunciados “jots”) são uma forma compacta e altamente portátil de trocar informações de identidade. A especificação JWT tem sido uma base importante para o OpenID Connect, fornecendo um token de logon único para o ecossistema OAuth 2.0. Os JWTs também podem ser usados como credenciais de autenticação por si só e são uma forma melhor de controlar o acesso a APIs baseadas na Web do que as chaves de API tradicionais.

O NGINX Plus R10 e versões posteriores podem validar JWTs diretamente. Nesta publicação do blog, descrevemos como usar o NGINX Plus como um gateway de API, fornecendo um frontend para um endpoint de API e usando JWTs para autenticar aplicações clientes.

O suporte nativo a JWT está disponível apenas no NGINX Plus, não no NGINX Open Source.

Editor – Esta publicação do blog foi atualizada em dezembro de 2021 para usar a diretiva auth_jwt_require introduzida no NGINX Plus R25. Para uma discussão detalhada sobre a diretiva, consulte Regras personalizadas de validação de JWT na publicação do blog que anuncia o NGINX Plus R25.

O NGINX Plus R15 e versões posteriores também podem controlar o “Fluxo de Código de Autorização” no OpenID Connect 1.0, o que permite a integração com a maioria dos principais provedores de identidade. Para obter detalhes, consulte Anúncio do NGINX Plus R15<.htmlspan>.

Anatomia de um JWT

Os JWTs têm três partes: um cabeçalho, uma carga útil e uma assinatura. Durante a transmissão, eles têm a aparência mostrada a seguir. Adicionamos quebras de linha para facilitar a leitura (o JWT real é uma única string) e usamos cores para diferenciar as três partes:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICAgInN1YiI6ICJsYzEiLAogICAgImVtYWlsIjogImxpYW0uY3JpbGx5QG5naW54LmNvbSIsCn0=.VGYHWPterIaLjRi0LywgN3jnDUQbSsFptUw99g2slfc

Como mostrado, um ponto ( . ) separa o cabeçalho, a carga útil e a assinatura. O cabeçalho e a carga útil são objetos JSON codificados em Base64. A assinatura é criptografada usando o algoritmo especificado pelo cabeçalho alg, como podemos ver ao decodificar nosso JWT de exemplo:

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

O padrão JWT define vários algoritmos de assinatura. O valor HS256 em nosso exemplo refere-se ao HMAC SHA-256, que usamos em todos os JWTs de exemplo desta publicação. O NGINX Plus é compatível com os algoritmos HSxxx, RSxxx e ESxxx de assinatura definidos no padrão. A capacidade de assinar JWTs criptograficamente os torna ideais para uso como credenciais de autenticação.

JWT como chave de API

Uma forma comum de autenticar um cliente de API (o cliente de software remoto que solicita recursos da API) é por meio de um segredo compartilhado, geralmente chamado de chave de API. Uma chave de API tradicional é, essencialmente, uma senha longa e complexa que o cliente envia como um cabeçalho HTTP adicional em todas as solicitações. O endpoint da API concede acesso ao recurso solicitado se a chave de API fornecida estiver na lista de chaves válidas. Em geral, o próprio endpoint da API não valida as chaves de API; em vez disso, um gateway de API gerencia o processo de autenticação e encaminha cada solicitação ao endpoint apropriado. Além de reduzir a carga computacional, isso oferece os benefícios de um proxy reverso, como alta disponibilidade e balanceamento de carga entre vários endpoints de API.

Cliente de API e autenticação JWT com uma chave de API tradicional
O gateway de API valida a chave de API consultando um registro de chaves antes de encaminhar a solicitação
para o endpoint da API

É comum aplicar diferentes controles de acesso e políticas a diferentes clientes de API. Com chaves de API tradicionais, é necessário realizar uma consulta para associar a chave de API a um conjunto de atributos. Realizar essa consulta em todas as solicitações tem um impacto considerável na latência geral do sistema. Com JWT, esses atributos são incorporados, eliminando a necessidade de uma consulta separada.

Usar JWT como chave de API oferece uma alternativa de alto desempenho às chaves de API tradicionais, combinando uma tecnologia de autenticação baseada nas melhores práticas com um esquema baseado em padrões para a troca de atributos de identidade.

Cliente de API e autenticação JWT com JWT e NGINX Plus
O NGINX Plus valida o JWT antes de encaminhar a solicitação aos endpoints da API

Configurando o NGINX Plus como um gateway de API de autenticação

A configuração do NGINX Plus para validar JWTs é muito simples.

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;
    }
}

A primeira coisa que fazemos é especificar, no bloco upstream, os endereços dos servidores que hospedam o endpoint da API. O bloco location especifica que todas as solicitações para URLs que começam com /products/ devem ser autenticadas. A diretiva auth_jwt define o realm de autenticação que será retornado (junto com um código de status 401) se a autenticação não for bem-sucedida.

A diretiva auth_jwt_key_file informa ao NGINX Plus como validar o elemento de assinatura do JWT. Neste exemplo, usamos o algoritmo HMAC SHA‑256 para assinar JWTs, portanto precisamos criar uma chave Web JSON em conf/api_secret.jwk para armazenar a chave simétrica usada na assinatura. O arquivo deve seguir o formato descrito pela especificação de Chave Web JSON; nosso exemplo é assim:

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

A chave simétrica é definida no campo k e este é o valor codificado em Base64URL da cadeia de caracteres em texto simples fantasticjwt. Obtivemos o valor codificado executando este comando:

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

O campo kty define o tipo de chave como uma chave simétrica (sequência de octetos). Por fim, o campo kid (ID da chave) define um número de série para esta chave Web JSON, neste caso, 0001, o que permite oferecer suporte a várias chaves no mesmo arquivo (especificado pela diretiva auth_jwt_key_file) e gerenciar o ciclo de vida dessas chaves e dos JWTs assinados com elas.

Agora estamos prontos para emitir JWTs para os clientes da nossa API.

Emitindo um JWT para clientes da API

Como exemplo de cliente de API, usaremos uma aplicação de “sistema de cotações” e criaremos um JWT para esse cliente. Primeiro, definimos o cabeçalho do JWT:

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

O campo typ define o tipo como JSON Web Token, o campo alg especifica que o JWT é assinado com o algoritmo HMAC SHA256, e o campo kid especifica que o JWT é assinado com a JSON Web Key que possui esse número de série.

Em seguida, definimos o payload do JWT:

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

O campo sub (assunto) é nosso identificador exclusivo para o valor completo do campo name. O campo iss descreve o emissor do JWT, o que é útil caso seu gateway de API também aceite JWTs de emissores terceiros ou de um sistema centralizado de gerenciamento de identidades.

Agora que temos tudo o que precisamos para criar o JWT, seguimos estas etapas para codificá-lo e assiná-lo corretamente. Os comandos e valores codificados aparecem em várias linhas apenas para facilitar a leitura; na prática, cada um deles é digitado ou exibido em uma única linha.

  1. Compacte separadamente e codifique em Base64URL o cabeçalho e a carga útil. 
$ 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. Concatene o cabeçalho e a carga útil codificados usando um ponto (.) e atribua o resultado à variável HEADER_PAYLOAD.
$ HEADER_PAYLOAD=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEifQ.eyJuYW1lIjoiUXVvdGF0aW9uIFN5c3RlbSIsInN1YiI6InF1b3RlcyIsIm lzcyI6Ik15IEFQSSBHYXRld2F5In0
  1. Assine o cabeçalho e a carga útil com nossa chave simétrica e codifique a assinatura em Base64URL.
$ echo -n $HEADER_PAYLOAD | openssl dgst -binary -sha256 -hmac fantasticjwt | base64 | tr '+/' '-_' | tr -d '='ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I
  1. Anexe a assinatura codificada ao cabeçalho e à carga útil.
$ echo $HEADER_PAYLOAD.ggVOHYnVFB8GVPE-VOIo3jD71gTkLffAY0hQOGXPL2I > quotes.jwt
  1. Faça um teste enviando uma solicitação autenticada ao gateway de API (neste exemplo, o gateway está sendo executado no localhost).
$ curl -H "Authorization: Bearer `cat quotes.jwt`" http://localhost/products/widget1

O comando curl na Etapa 5 envia o JWT ao NGINX Plus na forma de um Bearer Token, que é o formato esperado pelo NGINX Plus por padrão. O NGINX Plus também pode obter o JWT de um cookie ou parâmetro da string de consulta. Para configurar isso, inclua o parâmetro token= na diretiva auth_jwt. Por exemplo, com a configuração a seguir, o NGINX Plus pode validar o JWT enviado com 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;
    }
}

Depois de configurar o NGINX Plus e gerar e verificar um JWT conforme mostrado acima, você estará pronto para enviar o JWT ao desenvolvedor do cliente da API e definir o mecanismo que será usado para enviar o JWT em cada solicitação à API.

Como aproveitar as declarações JWT para registro em log e limitação de taxa

Uma das principais vantagens dos JWTs como credenciais de autenticação é que eles transmitem “declarações”, que representam entidades associadas ao JWT e à sua carga útil (por exemplo, o emissor, o usuário para o qual ele foi emitido e o destinatário pretendido). Após validar o JWT, o NGINX Plus tem acesso, como variáveis, a todos os campos presentes no cabeçalho e na carga útil. Para acessá-los, basta adicionar o prefixo $jwt_header_ ou $jwt_claim_ ao campo desejado (por exemplo, $jwt_claim_sub para a declaração sub). Isso significa que podemos encaminhar com muita facilidade as informações contidas no JWT para o endpoint da API, sem precisar implementar o processamento de JWT na própria API.

Este exemplo de configuração demonstra alguns dos recursos avançados.

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;
    }
}

A diretiva log_format define um novo formato chamado jwt, que estende o formato de log comum com dois campos adicionais, $jwt_header_alg e $jwt_claim_sub. Dentro do bloco location, usamos a diretiva access_log para gravar logs com os valores obtidos do JWT validado.

Neste exemplo, também usamos variáveis baseadas em declarações para limitar a taxa da API por cliente de API, em vez de por endereço IP. Isso é particularmente útil quando vários clientes de API estão incorporados a um único portal e não podem ser diferenciados pelo endereço IP. A diretiva limit_req_zone usa a declaração sub do JWT como chave para calcular os limites de taxa, que são então aplicados ao bloco location por meio da inclusão da diretiva limit_req.

Por fim, fornecemos o assunto do JWT como um novo cabeçalho HTTP quando a solicitação é encaminhada por proxy ao endpoint da API. A diretiva proxy_set_header adiciona um cabeçalho HTTP chamado API‑Client, que pode ser facilmente utilizado pelo endpoint da API. Portanto, o endpoint da API não precisa implementar nenhuma lógica de processamento de JWT. Isso se torna cada vez mais valioso à medida que o número de endpoints da API aumenta.

Revogando JWTs

De tempos em tempos, pode ser necessário revogar ou reemitir o JWT de um cliente de API. Ao combinar um bloco simples de map com a diretiva auth_jwt_require, podemos negar o acesso a um cliente de API marcando seu JWT como inválido até que a data de expiração do JWT (representada na declaração exp) seja atingida. Nesse momento, a entrada de map referente a esse JWT poderá ser removida com segurança.

Neste exemplo, definimos a variável $jwt_status como 0 ou 1 de acordo com o valor da declaração sub no token (conforme capturado na variável $jwt_claim_sub). Em seguida, usamos a diretiva auth_jwt_require no bloco location para validar (ou rejeitar) adicionalmente o token. Para que seja válido, a variável $jwt_status não pode estar vazia nem ser igual a 0 (zero).

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;
    }
}

Resumo

Os JSON Web Tokens são ideais para fornecer acesso autenticado a APIs. Para quem desenvolve clientes de API, eles são tão fáceis de usar quanto as chaves de API tradicionais e fornecem ao gateway de API informações de identidade que, de outra forma, exigiriam uma consulta ao banco de dados. O NGINX Plus oferece suporte à autenticação JWT e soluções avançadas de configuração baseadas nas informações contidas no próprio JWT. Combinado com outros recursos de gateway de API, o NGINX Plus permite fornecer serviços baseados em API com rapidez, confiabilidade, escalabilidade e segurança.

Para experimentar o JWT com o NGINX Plus, inicie sua avaliação gratuita de 30 dias hoje mesmo ou entre em contato conosco para conversar sobre seus casos de uso.


Compartilhar

Sobre o autor

Liam Crilly
Liam CrillyDiretor Sênior de Gestão de Produtos

Mais publicações de Liam Crilly

Publicações relacionadas no blog

Como o F5 NGINX One aprimora o gerenciamento e a observabilidade
NGINX | 10/10/2024

Como o F5 NGINX One aprimora o gerenciamento e a observabilidade

Descubra como o F5 NGINX One aprimora o gerenciamento e a observabilidade de ambientes híbridos e multicloud ao oferecer um console web centralizado baseado em SaaS, com insights em tempo real, conformidade automatizada e gerenciamento simplificado de configurações.

Do OpenAPI ao NGINX como gateway de API usando uma API declarativa
NGINX | 17/06/2024

Do OpenAPI ao NGINX como gateway de API usando uma API declarativa

Descubra como transformar uma definição de esquema OpenAPI em uma configuração NGINX totalmente funcional, operando como um API Gateway com a segurança de um Web Application Firewall e um Portal do Desenvolvedor, usando uma abordagem declarativa de API.

Como gerenciar picos repentinos de tráfego e sobrecarga do servidor
NGINX | 14/07/2021

Como gerenciar picos repentinos de tráfego e sobrecarga do servidor

Picos de tráfego e sobrecarga de servidores são preocupações sérias para varejistas online em qualquer época, mas especialmente durante a “Cyber Season” de compras de fim de ano. Detalhamos como você pode aproveitar as soluções de entrega de aplicações de nível de produção da NGINX para se preparar para aumentos repentinos de tráfego.

Distribuição segura de chaves privadas SSL com o NGINX
NGINX | 02/04/2019

Distribuição segura de chaves privadas SSL com o NGINX

Descrevemos três maneiras progressivamente mais seguras de proteger chaves privadas SSL ao configurar o NGINX para processar tráfego HTTPS: permitir acesso de leitura apenas ao usuário root, criptografar as chaves com senhas armazenadas separadamente e distribuir as senhas a partir de um repositório central.

Agrupando solicitações de API em lotes com NGINX Plus e o módulo JavaScript do NGINX
NGINX | 24/05/2018

Agrupando solicitações de API em lotes com NGINX Plus e o módulo JavaScript do NGINX

O módulo JavaScript do NGINX lançado com o NGINX Plus R15 oferece suporte a subsolicitações, possibilitando uma classe totalmente nova de casos de uso para o NGINX Plus. Este exemplo completo também usa a API do NGINX Plus e os módulos de chave-valor para converter solicitações de API dos clientes em solicitações separadas para as APIs dos microsserviços que compõem um aplicativo.

Como superar o esgotamento de portas efêmeras no NGINX e no NGINX Plus
NGINX | 19/04/2016

Como superar o esgotamento de portas efêmeras no NGINX e no NGINX Plus

Confira dicas para evitar o esgotamento de portas efêmeras com ajustes no kernel do Linux, conexões keepalive e conexões vinculadas dinamicamente a endereços IP locais.