Validando tokens de acesso OAuth 2.0 com NGINX e NGINX Plus

NGINX | 13 de maio de 2019

Imagem cortesia de John T. no unsplash.com

Há muitas opções para autenticar chamadas de API, desde certificados de cliente X.509 até a autenticação HTTP Basic. Nos últimos anos, porém, surgiu um padrão de fato na forma de tokens de acesso OAuth 2.0. Essas credenciais de autenticação são transmitidas do cliente para o servidor da API e normalmente enviadas em um cabeçalho HTTP.

No entanto, o OAuth 2.0 é um labirinto de padrões interligados. Os processos de emissão, apresentação e validação de um fluxo de autenticação OAuth 2.0 geralmente dependem de vários padrões relacionados. No momento da redação deste texto, existem oito padrões OAuth 2.0, e os tokens de acesso são um bom exemplo disso, pois a especificação principal do OAuth 2.0 (RFC 6749) não define um formato para tokens de acesso. Na prática, há dois formatos de uso comum:

  • JSON Web Token (JWT), conforme definido pela RFC 7519
  • Tokens opacos que são pouco mais do que um identificador exclusivo para um cliente autenticado

Após a autenticação, um cliente apresenta seu token de acesso em cada solicitação HTTP para obter acesso a recursos protegidos. É necessário validar o token de acesso para garantir que ele tenha sido realmente emitido por um provedor de identidade (IdP) confiável e que não tenha expirado. Como os IdPs assinam criptograficamente os JWTs que emitem, os JWTs podem ser validados “offline”, sem uma dependência do IdP em tempo de execução. Normalmente, um JWT também inclui uma data de expiração, que também pode ser verificada. O NGINX Plus auth_jwt módulo realiza a validação offline de JWTs.

Os tokens opacos, por outro lado, precisam ser validados por meio do reenvio ao IdP que os emitiu. No entanto, isso oferece a vantagem de permitir que esses tokens sejam revogados pelo IdP, por exemplo, como parte de uma operação de logout global, sem manter ativas as sessões iniciadas anteriormente. O logout global também pode tornar necessário validar os JWTs com o IdP.

Neste blog, descrevemos como o NGINX e o NGINX Plus podem atuar como uma Parte Confiável, enviando tokens de acesso ao IdP para validação e encaminhando por proxy apenas as solicitações aprovadas no processo de validação. Abordamos os diversos benefícios de usar o NGINX e o NGINX Plus para essa tarefa e como a experiência do usuário pode ser aprimorada armazenando em cache as respostas de validação por um curto período. Para o NGINX Plus, também mostramos como o cache pode ser distribuído por um cluster de instâncias do NGINX Plus, atualizando o armazenamento de chave-valor com o módulo JavaScript, conforme introduzido no NGINX Plus R18.

Exceto quando indicado, as informações deste blog se aplicam tanto ao NGINX Open Source quanto ao NGINX Plus. As referências ao NGINX Plus se aplicam somente a esse produto.

Introspecção de token

O método padrão para validar tokens de acesso com um IdP é chamado de introspecção de token. A RFC 7662, Introspecção de Token do OAuth 2.0, é atualmente um padrão amplamente adotado que descreve uma interface JSON/REST usada por uma Parte Confiável para apresentar um token ao IdP, bem como a estrutura da resposta. Esse padrão é compatível com muitos dos principais fornecedores de IdP e provedores de nuvem.

Independentemente do formato de token utilizado, realizar a validação em cada serviço de backend ou aplicação resulta em muito código duplicado e processamento desnecessário. É preciso considerar diversas condições de erro e casos extremos, e fazer isso em cada serviço de backend é uma receita para inconsistências na implementação e, consequentemente, uma experiência do usuário imprevisível. Considere como cada serviço de backend poderia lidar com as seguintes condições de erro:

  • Token de acesso ausente
  • Token de acesso extremamente grande
  • Caracteres inválidos ou inesperados no token de acesso
  • Vários tokens de acesso apresentados
  • Diferença de horário entre os serviços de backend

Aplicações de backend realizando a validação de tokens

Como usar o módulo auth_request do NGINX para validar tokens

Para evitar a duplicação de código e os problemas decorrentes, podemos usar o NGINX para validar tokens de acesso em nome dos serviços de backend. Isso oferece diversos benefícios:

  • As solicitações só chegam aos serviços de backend quando o cliente apresenta um token válido
  • Os serviços de backend existentes podem ser protegidos com tokens de acesso, sem a necessidade de alterações no código
  • Somente a instância do NGINX (e não cada aplicativo) precisa ser registrada no IdP
  • O comportamento é consistente para todas as condições de erro, incluindo tokens ausentes ou inválidos

NGINX realizando a validação de tokens como proxy reverso

Com o NGINX atuando como proxy reverso para uma ou mais aplicações, podemos usar o módulo auth_request module para acionar uma chamada de API para um IdP antes de encaminhar uma solicitação ao backend. Como veremos em breve, a solução a seguir tem uma falha fundamental, mas apresenta o funcionamento básico do módulo auth_request, que abordaremos em mais detalhes nas seções posteriores.

Carregando exemplo de código…

A diretiva auth_request (linha 5) especifica o local para processar as chamadas de API. O proxy para o backend (linha 6) ocorre somente se a resposta de auth_request for bem-sucedida. O local de auth_request é definido na linha 9. Ele é marcado como internal para impedir que clientes externos o acessem diretamente.

As linhas 11–14 definem vários atributos da solicitação para que ela esteja em conformidade com o formato de solicitação de introspecção de token. Observe que o token de acesso enviado na solicitação de introspecção é um componente do corpo definido na linha 14. Aqui, token=$http_apikey indica que o cliente deve fornecer o token de acesso no cabeçalho de solicitação apikey. Naturalmente, o token de acesso pode ser fornecido em qualquer atributo da solicitação; nesse caso, usamos uma variável diferente do NGINX.

Estendendo o auth_request com o módulo JavaScript do NGINX

Como mencionado, usar o módulo auth_request dessa forma não é uma solução completa. O módulo auth_request usa códigos de status HTTP para determinar o sucesso (2xx = sucesso, 4xx = falha). No entanto, as respostas de introspecção de token OAuth 2.0 indicam sucesso ou falha em um objeto JSON e retornam o código de status HTTP 200 (OK) em ambos os casos.

Formato JSON da resposta de introspecção de token para um token válido

O que precisamos é de um analisador JSON que converta a resposta de introspecção do IdP no código de status HTTP apropriado, para que o módulo auth_request possa interpretar corretamente essa resposta.

Felizmente, a análise de JSON é uma tarefa trivial para o módulo JavaScript do NGINX (njs). Portanto, em vez de definir um bloco location para realizar a solicitação de introspecção do token, instruímos o módulo auth_request a chamar uma função JavaScript.

[Editor – Este artigo é um dos vários que exploram casos de uso do módulo JavaScript do NGINX. Para ver a lista completa, consulte Casos de uso do módulo JavaScript do NGINX.

O código nesta seção foi atualizado para usar a diretiva js_import, que substitui a diretiva js_include no NGINX Plus R23 e em versões posteriores. Para obter mais informações, consulte a documentação de referência do módulo JavaScript do NGINX — a seção Exemplo de configuração mostra a sintaxe correta para os arquivos de configuração do NGINX e JavaScript.]

Observação: Esta solução requer que o módulo JavaScript seja carregado como um módulo dinâmico com a diretiva load_module no arquivo nginx.conf. Para obter instruções, consulte o Guia de Administração do NGINX Plus.

Carregando exemplo de código…

A diretiva js_content na linha 13 especifica uma função JavaScript, introspectAccessToken, como manipulador de auth_request. A função do manipulador é definida em oauth2.js:

Carregando exemplo de código…

Observe que a função introspectAccessToken faz uma subsolicitação HTTP (linha 2) para outro local (/oauth2_send_request), definido no trecho de configuração abaixo. Em seguida, o código JavaScript analisa a resposta (linha 5) e envia o código de status apropriado de volta ao módulo auth_request com base no valor do campo active. Tokens válidos (ativos) retornam HTTP 204 (Sem conteúdo) (mas com êxito), enquanto tokens inválidos retornam HTTP 403 (Proibido). Em condições de erro, é retornado HTTP 401 (Não autorizado), permitindo distinguir erros de tokens inválidos.

Observação: Este código é fornecido apenas como prova de conceito e não está pronto para uso em produção. Uma solução completa, com tratamento abrangente de erros e registro em logs, é apresentada abaixo.

O local de destino da subsolicitação definido na linha 2 é muito semelhante à nossa configuração original de auth_request.

Carregando exemplo de código…

Toda a configuração necessária para criar a solicitação de introspecção de token está contida no local /_oauth2_send_request. A autenticação (linha 19), o próprio token de acesso (linha 21) e a URL do endpoint de introspecção de token (linha 22) geralmente são os únicos itens de configuração necessários. A autenticação é necessária para que o IdP aceite solicitações de introspecção de token desta instância do NGINX. A especificação Introspecção de Token do OAuth 2.0 exige autenticação, mas não especifica o método. Neste exemplo, usamos um token de portador no cabeçalho Authorization.

Com essa configuração implementada, quando o NGINX recebe uma solicitação, ele a encaminha ao módulo JavaScript, que faz uma solicitação de introspecção de token ao IdP. A resposta do IdP é analisada, e a autenticação é considerada bem-sucedida quando o campo active é true. Essa solução é uma maneira compacta e eficiente de realizar a introspecção de tokens OAuth 2.0 com o NGINX e pode ser facilmente adaptada para outras APIs de autenticação.

Mas ainda não terminamos. O maior desafio da introspecção de tokens em geral é que ela adiciona latência a todas as solicitações HTTP. Isso pode se tornar um problema significativo quando o IdP em questão é uma solução hospedada ou um provedor de nuvem. O NGINX e o NGINX Plus podem minimizar essa desvantagem armazenando em cache as respostas de introspecção.

Otimização 1: armazenamento em cache pelo NGINX

A introspecção de tokens OAuth 2.0 é fornecida pelo IdP em um endpoint JSON/REST; portanto, a resposta padrão é um corpo JSON com o status HTTP 200. Quando essa resposta é associada ao token de acesso, ela se torna altamente armazenável em cache.

Resposta completa da introspecção de token para um token válido

O NGINX pode ser configurado para armazenar em cache uma cópia da resposta de introspecção de cada token de acesso. Assim, na próxima vez que o mesmo token de acesso for apresentado, o NGINX fornecerá a resposta de introspecção armazenada em cache em vez de fazer uma chamada de API ao IdP. Isso reduz significativamente a latência geral das solicitações subsequentes. Podemos controlar por quanto tempo as respostas armazenadas em cache serão usadas para reduzir o risco de aceitar um token de acesso expirado ou revogado recentemente. Por exemplo, se um cliente de API normalmente fizer uma sequência de várias chamadas de API em um curto período, uma validade de cache de 10 segundos poderá ser suficiente para proporcionar uma melhoria perceptível na experiência do usuário.

O cache é habilitado ao especificar seu armazenamento: um diretório em disco para o cache (respostas de introspecção) e uma zona de memória compartilhada para as chaves (tokens de acesso).

Carregando exemplo de código…

A diretiva proxy_cache_path aloca o armazenamento necessário: /var/cache/nginx/oauth para as respostas de introspecção e uma zona de memória chamada token_responses para as chaves. Ela é configurada no contexto http e, portanto, aparece fora dos blocos server e location. O cache é então habilitado dentro do bloco location em que as respostas de introspecção de token são processadas:

Carregando exemplo de código…

O cache está habilitado para esse local com a diretiva proxy_cache (linha 26). Por padrão, o NGINX armazena em cache com base no URI, mas, no nosso caso, queremos armazenar a resposta em cache com base no token de acesso fornecido no cabeçalho de solicitação apikey (linha 27).

Na linha 28, usamos a diretiva proxy_cache_lock para informar ao NGINX que, se solicitações simultâneas chegarem com a mesma chave de cache, ele deverá aguardar até que a primeira solicitação tenha preenchido o cache antes de responder às demais. A diretiva proxy_cache_valid (linha 29) informa ao NGINX por quanto tempo a resposta de introspecção deve ser armazenada em cache. Sem essa diretiva, o NGINX determina o tempo de armazenamento em cache com base nos cabeçalhos de controle de cache enviados pelo IdP. No entanto, eles nem sempre são confiáveis, por isso também instruímos o NGINX a ignorar os cabeçalhos que, de outra forma, afetariam a maneira como armazenamos as respostas em cache (linha 30).

Agora que o cache está habilitado, um cliente que apresenta um token de acesso sofre apenas o impacto da latência da solicitação de introspecção do token uma vez a cada 10 segundos.

Otimização 2: cache distribuído com NGINX Plus

Combinar o cache de conteúdo com a introspecção de tokens é uma maneira altamente eficaz de melhorar o desempenho geral da aplicação com um impacto insignificante na segurança. No entanto, se o NGINX for implantado de forma distribuída — por exemplo, em vários data centers, plataformas de nuvem ou em um cluster ativo-ativo —, as respostas de introspecção de tokens armazenadas em cache ficarão disponíveis apenas para a instância do NGINX que realizou a solicitação de introspecção.

Com o NGINX Plus, podemos usar o módulo keyval module – um armazenamento de chave-valor na memória – para armazenar em cache as respostas de introspecção de token. Além disso, também podemos sincronizar essas respostas em um cluster de instâncias do NGINX Plus usando o módulo zone_sync module. Isso significa que, independentemente de qual instância do NGINX Plus tenha realizado a solicitação de introspecção de token, a resposta estará disponível em todas as instâncias do NGINX Plus no cluster.

Observação: A configuração do módulo zone_sync para o compartilhamento do estado de execução está fora do escopo deste blog. Para obter mais informações sobre o compartilhamento de estado em um cluster do NGINX Plus, consulte o Guia de Administração do NGINX Plus.

No NGINX Plus R18 e versões posteriores, o armazenamento de chave-valor pode ser atualizado modificando-se a variável declarada na diretiva keyval. Como o módulo JavaScript tem acesso a todas as variáveis do NGINX, isso permite que as respostas de introspecção sejam inseridas no armazenamento de chave-valor durante o processamento da resposta.

Assim como o cache do sistema de arquivos do NGINX, o armazenamento de chave-valor é habilitado ao especificar seu local de armazenamento — neste caso, uma zona de memória que armazena a chave (token de acesso) e o valor (resposta de introspecção).

Carregando exemplo de código…

Observe que, com o parâmetro timeout da diretiva keyval_zone, especificamos o mesmo período de validade de 10 segundos para as respostas armazenadas em cache que o definido na linha 29 de auth_request_cache.conf, para que cada membro do cluster do NGINX Plus remova a resposta de forma independente quando ela expirar. A linha 2 especifica o par chave-valor de cada entrada: a chave é o token de acesso fornecido no cabeçalho da solicitação apikey, e o valor é a resposta de introspecção conforme avaliada pela variável $token_data.

Agora, para cada solicitação que inclui um cabeçalho de solicitação apikey, a variável $token_data é preenchida com a resposta anterior de introspecção do token, caso exista. Portanto, atualizamos o código JavaScript para verificar se já temos uma resposta de introspecção do token.

Carregando exemplo de código…

A linha 2 verifica se já existe uma entrada no armazenamento de chave-valor para esse token de acesso. Como há duas maneiras de obter uma resposta de introspecção (do armazenamento de chave-valor ou de uma resposta de introspecção), movemos a lógica de validação para a seguinte função separada, tokenResult:

Carregando exemplo de código…

Agora, cada resposta de introspecção de token é salva no armazenamento de chave-valor e sincronizada com todos os outros membros do cluster do NGINX Plus. O exemplo a seguir mostra uma solicitação HTTP simples com um token de acesso válido, seguida de uma consulta à API do NGINX Plus para exibir o conteúdo do armazenamento de chave-valor.

$ curl -IH "apikey: tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA" http://localhost/HTTP/1.1 200 OK
Date: Wed, 24 Apr 2019 17:41:34 GMT
Content-Type: application/json
Content-Length: 612

$ curl http://localhost/api/4/http/keyvals/access_tokens
{"tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA":"{\"active\":true}"}

Observe que o próprio armazenamento de chave-valor usa o formato JSON, portanto, as aspas na resposta de introspecção do token são automaticamente escapadas.

Otimização 3: Extração de atributos da resposta de introspecção

Um recurso útil da introspecção de tokens do OAuth 2.0 é que a resposta pode conter informações sobre o token além de seu status de atividade. Essas informações incluem a data de expiração do token e atributos do usuário associado, como nome de usuário, endereço de e-mail, entre outros.

Resposta da introspecção de token com atributos do token

Essas informações adicionais podem ser muito úteis. Elas podem ser registradas, usadas para implementar políticas de controle de acesso granulares ou fornecidas a aplicações de backend. Podemos exportar cada um desses atributos para o módulo auth_request enviando-os como cabeçalhos de resposta adicionais em uma resposta bem-sucedida (HTTP 204).

Carregando exemplo de código…

Iteramos sobre cada atributo da resposta de introspecção (linha 23) e o enviamos de volta ao módulo auth_request module como um cabeçalho de resposta. Cada nome de cabeçalho recebe o prefixo Token- para evitar conflitos com cabeçalhos de resposta padrão (linha 26). Esses cabeçalhos de resposta agora podem ser convertidos em variáveis do NGINX e usados como parte da configuração normal.

Carregando exemplo de código…

Neste exemplo, convertemos o atributo username em uma nova variável, $username (linha 11). A diretiva auth_request_set permite exportar o contexto da resposta de introspecção do token para o contexto da solicitação atual. O cabeçalho de resposta de cada atributo (adicionado pelo código JavaScript) fica disponível como $sent_http_token_attribute. Em seguida, a linha 12 inclui o valor de $username como um cabeçalho de solicitação encaminhado ao backend. Podemos repetir essa configuração para qualquer um dos atributos retornados na resposta de introspecção do token.

Exportando atributos da resposta de introspecção do token para a solicitação encaminhada por proxy

Configuração de produção

Os exemplos de código e configuração acima são funcionais e adequados para testes de prova de conceito ou personalização para um caso de uso específico. Para uso em produção, recomendamos enfaticamente a inclusão de tratamento de erros adicional, registro em logs e configuração flexível. Você pode encontrar uma implementação mais robusta e detalhada para NGINX e NGINX Plus em nosso repositório no GitHub:

Resumo

Neste blog, mostramos como usar o módulo auth_request do NGINX em conjunto com o módulo JavaScript para realizar a introspecção de tokens OAuth 2.0 nas solicitações dos clientes. Além disso, ampliamos essa solução com o uso de cache e extraímos atributos da resposta de introspecção para uso na configuração do NGINX.

Também descrevemos como o armazenamento de chave-valor do NGINX Plus pode ser usado como um cache distribuído para respostas de introspecção, adequado para implantações em produção em um cluster de instâncias do NGINX Plus.

Experimente você mesmo a introspecção de tokens OAuth 2.0 com o NGINX Plus — comece 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.