BLOG | NGINX

Validando tokens de acesso OAuth 2.0 com NGINX e NGINX Plus

NGINX-Parte-de-F5-horiz-preto-tipo-RGB
Miniatura de Liam Crilly
Liam Crilly
Publicado em 13 de maio de 2019

 

Imagem cortesia de John T. em unsplash.com

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

O OAuth 2.0, no entanto, é um labirinto de padrões interconectados. Os processos para emitir, apresentar e validar um fluxo de autenticação OAuth 2.0 geralmente dependem de vários padrões relacionados. No momento em que este artigo foi escrito, havia oito padrões OAuth 2.0 , e os tokens de acesso são um exemplo disso, já que a especificação principal do OAuth 2.0 ( RFC 6749 ) não especifica um formato para tokens de acesso. No mundo real, há dois formatos de uso comum:

  • JSON Web Token (JWT) conforme definido pelo 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 com cada solicitação HTTP para obter acesso a recursos protegidos. A validação do token de acesso é necessária para garantir que ele foi realmente emitido por um provedor de identidade confiável (IdP) e que não expirou. Como os IdPs assinam criptograficamente os JWTs que emitem, os JWTs podem ser validados “offline” sem uma dependência de tempo de execução no IdP. Normalmente, um JWT também inclui uma data de validade que também pode ser verificada. O módulo auth_jwt do NGINX Plus executa a validação offline do JWT.

Os tokens opacos, por outro lado, devem ser validados enviando-os de volta ao IdP que os emitiu. No entanto, isso tem a vantagem de que tais tokens podem ser revogados pelo IdP, por exemplo, como parte de uma operação de logout global, sem deixar sessões previamente conectadas ainda ativas. O logout global também pode tornar necessário validar JWTs com o IdP.

Neste blog, descrevemos como o NGINX e o NGINX Plus podem atuar como uma Parte Confiável do OAuth 2.0, enviando tokens de acesso ao IdP para validação e fazendo proxy apenas de solicitações que passam pelo processo de validação. Discutiremos os vários benefícios de usar o NGINX e o NGINX Plus para essa tarefa e como a experiência do usuário pode ser melhorada 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 em 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 neste blog se aplicam ao NGINX Open Source e ao NGINX Plus. Referências ao NGINX Plus aplicam-se 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 tokens . RFC 7662 , OAuth 2.0 Token Introspection , agora é um padrão amplamente suportado que descreve uma interface JSON/REST que uma Parte Confiável usa para apresentar um token ao IdP e descreve a estrutura da resposta. Ele é suportado por muitos dos principais fornecedores de IdP e provedores de nuvem.

Independentemente do formato de token usado, executar a validação em cada serviço ou aplicativo de backend resulta em muito código duplicado e processamento desnecessário. Várias condições de erro e casos extremos precisam ser considerados, e fazer isso em cada serviço de backend é uma receita para inconsistência na implementação e, consequentemente, uma experiência imprevisível para o usuário. Considere como cada serviço de backend pode 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
  • Desvio de relógio em serviços de backend
Aplicativos de backend realizando validação de token

Usando o módulo NGINX auth_request para validar tokens

Para evitar duplicação de código e os problemas resultantes, podemos usar o NGINX para validar tokens de acesso em nome de serviços de backend. Isso tem uma série de benefícios:

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

Com o NGINX atuando como um proxy reverso para um ou mais aplicativos, podemos usar o módulo auth_request para acionar uma chamada de API para um IdP antes de enviar uma solicitação ao backend. Como veremos em breve, a solução a seguir tem uma falha fundamental, mas introduz a operação básica do módulo auth_request , que expandiremos em seções posteriores.

 

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

As linhas 11 a 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 da solicitação apikey . Claro, o token de acesso pode ser fornecido em qualquer atributo da solicitação, nesse caso usamos uma variável NGINX diferente.

Estendendo auth_request com o módulo JavaScript NGINX

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

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

O que precisamos é de um analisador JSON para converter 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 NGINX (njs). Então, em vez de definir um bloco de localização para executar a solicitação de introspecção de token, dizemos ao módulo auth_request para chamar uma função JavaScript.

[ Editor – Esta postagem é uma das várias que exploram casos de uso para o módulo NGINX JavaScript. Para uma lista completa, consulte Casos de uso para o módulo JavaScript 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 posteriores. Para obter mais informações, consulte a documentação de referência do módulo NGINX JavaScript – a seção Configuração de exemplo mostra a sintaxe correta para a configuração do NGINX e arquivos 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 em nginx.conf . Para obter instruções, consulte o Guia de administração do NGINX Plus .

 

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

 

Observe que a função introspectAccessToken faz uma sub-requisição HTTP (linha 2) para outro local ( /oauth2_send_request ), que é definido no snippet de configuração abaixo. O código JavaScript então 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 ativo . Tokens válidos (ativos) retornam HTTP204 (Sem conteúdo) (mas sucesso) e tokens inválidos retornam HTTP403 (Proibido) . Condições de erro retornam HTTP401 (Não autorizado) para que erros possam ser distinguidos de tokens inválidos.

Observação:  Este código é fornecido apenas como prova de conceito e não é de qualidade de produção. Uma solução completa com tratamento e registro de erros abrangentes é fornecida abaixo .

O local de destino da sub-solicitação definido na linha 2 se parece muito com nossa configuração auth_request original.

 

Toda a configuração para construir 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 para o ponto de extremidade de introspecção do token (linha 22) são normalmente 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 NGINX. A especificação de introspecção de token do OAuth 2.0 exige autenticação, mas não especifica o método. Neste exemplo, usamos um token portador no cabeçalho de autorização .

Com essa configuração em vigor, quando o NGINX recebe uma solicitação, ele a passa para o módulo JavaScript, que faz uma solicitação de introspecção de token no IdP. A resposta do IdP é inspecionada e a autenticação é considerada bem-sucedida quando o campo ativo é true . Esta solução é uma maneira compacta e eficiente de executar 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 com a introspecção de tokens em geral é que ela adiciona latência a cada solicitação 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 oferecer otimizações para essa desvantagem armazenando em cache as respostas de introspecção.

Otimização 1: Cache por NGINX

A introspecção do token OAuth 2.0 é fornecida pelo IdP em um ponto de extremidade JSON/REST e, portanto, a resposta padrão é um corpo JSON com status HTTP200 . Quando essa resposta é vinculada ao token de acesso, ela se torna altamente armazenável em cache.

Resposta completa de 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 para cada token de acesso, de modo que, na próxima vez que o mesmo token de acesso for apresentado, o NGINX forneça a resposta de introspecção armazenada em cache em vez de fazer uma chamada de API para o IdP. Isso melhora muito a latência geral para solicitações subsequentes. Podemos controlar por quanto tempo as respostas armazenadas em cache são usadas para mitigar o risco de aceitar um token de acesso expirado ou revogado recentemente. Por exemplo, se um cliente de API normalmente faz uma série de chamadas de API em um curto período de tempo, uma validade de cache de 10 segundos pode ser suficiente para fornecer uma melhoria mensurável na experiência do usuário.

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

 

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. Ele é configurado no contexto http e, portanto, aparece fora dos blocos de servidor e localização . O cache em si é então habilitado dentro do bloco de localização onde as respostas de introspecção do token são processadas:

 

O cache é habilitado para este 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 em cache a resposta com base no token de acesso apresentado no cabeçalho da 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 precisará aguardar até que a primeira solicitação tenha preenchido o cache antes de responder às outras. A diretiva proxy_cache_valid (linha 29) informa ao NGINX por quanto tempo armazenar em cache a resposta de introspecção. Sem essa diretiva, o NGINX determina o tempo de armazenamento em cache a partir dos cabeçalhos de controle de cache enviados pelo IdP; no entanto, eles nem sempre são confiáveis, e é por isso que também dizemos ao NGINX para ignorar cabeçalhos que, de outra forma, afetariam a forma como armazenamos as respostas em cache (linha 30).

Com o cache agora habilitado, um cliente que apresenta um token de acesso sofre apenas o custo de latência de fazer a 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 do aplicativo 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 um cluster ativo-ativo – as respostas de introspecção de token em cache estarão disponíveis apenas para a instância do NGINX que executou a solicitação de introspecção.

Com o NGINX Plus, podemos usar o módulo keyval – um armazenamento de chave-valor na memória – para armazenar em cache respostas de introspecção de tokens. Além disso, também podemos sincronizar essas respostas em um cluster de instâncias do NGINX Plus usando o módulo zone_sync . Isso significa que não importa qual instância do NGINX Plus executou 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 compartilhamento de estado de tempo de execução está fora do escopo deste blog. Para obter mais informações sobre como compartilhar o estado em um cluster NGINX Plus, consulte o Guia de administração do NGINX Plus .

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

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

 

Observe que com o parâmetro timeout da diretiva keyval_zone especificamos o mesmo período de validade de 10 segundos para respostas armazenadas em cache como na linha 29 de auth_request_cache.conf , para que cada membro do cluster NGINX Plus remova a resposta independentemente quando ela expirar. A linha 2 especifica o par chave-valor para cada entrada: a chave é o token de acesso fornecido no cabeçalho da solicitação apikey e o valor é a resposta de introspecção 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 de introspecção de token anterior, se houver. Portanto, atualizamos o código JavaScript para verificar se já temos uma resposta de introspecção de token.

 

A linha 2 testa se já existe uma entrada de armazenamento de chave-valor para este token de acesso. Como há dois caminhos pelos quais uma resposta de introspecção pode ser obtida (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 :

 

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

$ curl -IH "apikey: tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA" http://localhost/ HTTP/1.1 200 OK Data: Qua, 24 Abr 2019 17:41:34 GMT Tipo de conteúdo: application/json Comprimento do conteúdo: 612 $ curl http://localhost/api/4/http/keyvals/access_tokens {"tQ7AfuEFvI1yI-XNPNhjT38vg_reGkpDFA":"{\"ativo\":true}"}

Observe que o armazenamento de chave-valor usa o formato JSON, portanto, a resposta de introspecção do token tem escape aplicado automaticamente às aspas.

Otimização 3: Extraindo 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 ativo. Essas informações incluem a data de expiração do token e os atributos do usuário associado: nome de usuário, endereço de e-mail e assim por diante.

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

Essas informações adicionais podem ser muito úteis. Ele pode ser registrado, usado para implementar políticas de controle de acesso refinadas ou fornecido para aplicativos de back-end. Podemos exportar cada um desses atributos para o módulo auth_request enviando-os como cabeçalhos de resposta adicionais com um sucesso (HTTP204 ) resposta.

 

Iteramos sobre cada atributo da resposta de introspecção (linha 23) e o enviamos de volta ao módulo auth_request como um cabeçalho de resposta. Cada nome de cabeçalho é prefixado com 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 NGINX e usados como parte da configuração regular.

 

Neste exemplo, convertemos o atributo username em uma nova variável, $username (linha 11). A diretiva auth_request_set nos permite exportar o contexto da resposta de introspecção do token para o contexto da solicitação atual. O cabeçalho de resposta para cada atributo (adicionado pelo código JavaScript) está disponível como $sent_http_token_ attribute . A linha 12 inclui o valor de $username como um cabeçalho de solicitação que é enviado por proxy para o 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 com 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 fortemente tratamento de erros adicional, registro e configuração flexível. Você pode encontrar uma implementação mais robusta e detalhada para NGINX e NGINX Plus em nosso repositório GitHub:

Resumo

Neste blog, mostramos como usar o módulo auth_request do NGINX em conjunto com o módulo JavaScript para executar a introspecção de token OAuth 2.0 em solicitações de clientes. Além disso, estendemos essa solução com 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 de 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 seu teste gratuito de 30 dias hoje mesmo ou entre em contato conosco para discutir seus casos de uso .


"Esta postagem do blog pode fazer referência a produtos que não estão mais disponíveis e/ou não têm mais suporte. Para obter as informações mais atualizadas sobre os produtos e soluções F5 NGINX disponíveis, explore nossa família de produtos NGINX . O NGINX agora faz parte do F5. Todos os links anteriores do NGINX.com redirecionarão para conteúdo semelhante do NGINX no F5.com."