Table of Contents
Este tutorial mostra como proteger AS APIs construídas em Portal da API da Amazon mais segura do que nunca, utilizando “tokens de acesso vinculados a certificados”.
Uma vez que um token de acesso tradicional do OAuth é vazado, um invasor pode acessar APIs com o token de acesso. Os tokens de acesso tradicionais são como uma passagem de trem que qualquer pessoa pode usar uma vez que é roubada.
A vulnerabilidade pode ser atenuada exigindo que o chamador de API apresente não apenas um token de acesso, mas também evidências que comprovem que o chamador de API é o titular legítimo do token de acesso. A evidência é chamada de “prova de posse”, que muitas vezes é encurtado para Pop. Os tokens de acesso que precisam de PoP em seu uso são como uma passagem de avião para voo internacional cujo procedimento de embarque exige que o passageiro apresente não apenas o bilhete, mas também seu passaporte.
RFC 8705, OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens, padronizou um mecanismo PoP. O mecanismo é chamado MTLS na comunidade OAuth, mas eu pessoalmente chamá-lo “vinculação de certificado” para evitar confusão. De qualquer forma, em suma, o mecanismo requer que o chamador de API apresente não apenas um token de acesso, mas também o mesmo Certificado X.509 como foi usado quando o token de acesso foi emitido a partir do ponto final do token. O diagrama abaixo ilustra o conceito de vinculação de certificado.
Você pode saber API de grau financeiro (FAPI). É uma especificação construída em cima de OAuth 2.0 e OpenID Connect para maior segurança da API. UK Open Banking adotou a FAPI como base do Perfil de Open Banking e agora outros países estão seguindo. O ponto que eu quero que você preste atenção aqui é que a FAPI requer vinculação de certificado como um componente técnico obrigatório.
A principal premissa para a vinculação cetificate é que as conexões entre APIs e aplicativos de clientes são estabelecidas usando TLS mútuo onde os aplicativos do cliente são necessários para apresentar seu X.509 certificado de cliente durante o aperto de mão TLS. Em 17 de setembro de 2020, a AWS finalmente anunciou em "Introdução de autenticação TLS mútua para Amazon API Gateway" que o Amazon API Gateway se tornou pronto para tls mútuo. O Amazon API Gateway abriu uma porta para a segurança da API de nível financeiro.
Amazon API Gateway fornece um mecanismo chamado Autorizador Lambda pelo qual você pode implementar a lógica personalizada para a proteção da API. Um autorizador lambda que oferece proteção API baseada em OAuth extrai um token de acesso e um certificado de cliente de uma chamada de API e executa a seguinte validação.
Em seguida, o autorizador toma uma das seguintes ações com base no resultado da validação.
'Unauthorized'
para dizer ao Amazon API Gateway para rejeitar o acesso ao recurso com código de status HTTP “401 não autorizado”.A implementação do autorizador lambda eu vou mostrar-lhe em breve delegados validação de token de acesso para AuthleteAPI de introspecção (/api/auth/introspection
), para que a implementação do autorizador não contenha lógica complexa. O diagrama abaixo retrata a relação entre Amazon API Gateway, Lambda Authorizer e Authlete.
Além disso, porque Authorizer classe em Biblioteca Python de Authlete faz quase todas as coisas necessárias, implementações podem ser muito pequenas. Na verdade, o código abaixo é um exemplo completo da implementação do autorizador Lambda que suporta a vinculação do certificado.
from authlete.aws.apigateway.authorizer import Authorizer
authorizer = Authorizer()
def lambda_handler(event, context):
return authorizer.handle(event, context)
Em casos reais, no entanto, você precisará configurar qual recurso requer quais escopos. Isso pode ser alcançado por (1) dando uma função para Authorizer
’s handle()
método ou (2) fazendo uma subclasse de Authorizer
e substituindo determine_scopes()
método na subclasse.
Tanto a função quanto o método levam 4 argumentos conforme listado na tabela abaixo e são obrigados a retornar uma lista de nomes de escopo necessários para acessar o recurso.
Parametro | Descrição |
---|---|
event |
O evento foi dado ao autorizador. |
context |
O contexto dado ao autorizador. |
method |
O método HTTP do acesso ao recurso. |
path |
O caminho do recurso. |
Dois exemplos abaixo têm o mesmo efeito afirmando que time:query
escopo é necessário para acessar time
recurso por HTTP GET
método.
from authlete.aws.apigateway.authorizer import Authorizer
authorizer = Authorizer()
def determine_scopes(event, context, method, path):
if method == 'GET' and path == 'time':
return ['time:query']
return None
def lambda_handler(event, context):
return authorizer.handle(event, context, determine_scopes)
from authlete.aws.apigateway.authorizer import Authorizer
class CustomAuthorizer(Authorizer):
def determine_scopes(self, event, context, method, path):
if method == 'GET' and path == 'time':
return ['time:query']
return None
authorizer = CustomAuthorizer()
def lambda_handler(event, context):
return authorizer.handle(event, context)
Para estar em conformidade com as especificações relacionadas ao OAuth 2.0, um autorizador da Lambda tem que dizer ao Amazon API Gateway para devolver “401 não autorizados” ao chamador de API em alguns casos (por exemplo, quando o token de acesso apresentado expirou). De acordo com os documentos técnicos e programas de amostra da AWS, o autorizador tem que lançar uma exceção com uma mensagem 'Unauthorized'
para alcançá-lo. No entanto, essa simples exceção deixa cair todas as informações valiosas sobre a resposta “não autorizada” e torna a depuração muito difícil.
Portanto, por padrão, em outras palavras, quando policy
propriedade é True
(padrão), Authorizer
’s handle()
método sempre retorna uma política IAM que representa “Permitir” ou “Negar” mesmo em casos de erro em que uma exceção com uma mensagem 'Unauthorized'
deve ser jogado.
Se você quiser fazer Authorizer
lançar uma exceção nos casos “Não autorizado” e “Erro do Servidor Interno”, definido False
Para policy
propriedade. Você pode alcançá-lo dando policy=False
Para Authorizer
‘construtor.
authorizer = Authorizer(policy=False)
Authorizer
’s handle()
método retorna um dict
exemplo que representa uma política IAM. No dicionário, há context
chave cujo valor é um dicionário. Authorizer
incorpora algumas informações lá. A tabela abaixo mostra chaves que context
dicionário pode conter.
Propriedade | descrição |
---|---|
introspection_request |
String JSON que representa o pedido para a API de introspecção da Authlete. |
introspection_response |
JSON string que representa a resposta da API de introspecção da Authlete. |
introspection_exception |
String que representa uma exceção levantada durante a chamada para a API de introspecção da Authlete. |
scope |
String de uma lista de escopos delimitados pelo espaço coberto pelo token de acesso apresentado. |
client_id |
A ID do cliente do aplicativo do cliente ao qual o token de acesso foi emitido. |
sub |
String que representa o assunto do proprietário do recurso que permitiu a emissão do token de acesso ao aplicativo do cliente. |
exp |
A data de validade do token de acesso em segundos desde a época unix (1 de janeiro de 1970). |
challenge |
O valor para WWW-Authenticate Cabeçalho HTTP em casos de erro. |
action |
O valor de action na resposta da API de introspecção do Authlete. |
resultMessage |
O valor de resultMessage na resposta da API de introspecção do Authlete. |
Você pode adicionar entradas a context
por substituir update_policy_context()
método em uma subclasse de Authorizer
. Tenha cuidado para não usar objeto JSON e matriz como valores em context
. Trata-se de uma restrição técnica imposta pela AWS.
class CustomAuthorizer(Authorizer):
def update_policy_context(self, event, context, request, response, exception, ctx):
ctx.update({
'my_key': 'my_value'
})
Inscrições em context
na apólice devolvida de um autorizador Lambda pode ser usado em outros lugares mais tarde. Ver "Saída de um autorizador Lambda do Amazon API Gateway" para detalhes.
Authorizer
classe oferece seguintes métodos de gancho para implementações de subclasse. Substitua-os conforme necessário.
Método | descrição |
---|---|
determine_scopes() |
determina os escopos necessários para o acesso ao recurso. |
update_policy_context() |
Atualizações context que deve ser incorporado na política. |
on_enter() |
é chamado quando handle() método começa. |
on_introspection_error() |
é chamada quando a chamada para API de introspecção Authlete falhou. |
on_introspection() |
é chamada depois que a API de introspecção Authlete foi bem sucedida. |
on_allow() |
é chamado quando uma política permitir é gerada. |
on_deny() |
é chamado quando uma política de Nega é gerada. |
on_unauthorized() |
é chamado quando uma exceção para “Não Autorizado” é lançada. |
on_internal_server_error() |
é chamado quando uma exceção para “Erro do servidor interno” é lançada. |
A coisa mais importante na criação de um autorizador Lambda é escolha “Solicitar” para o Payload do Evento Lambda. Caso contrário, o autorizador não pode acessar informações sobre o certificado do cliente. Isso significa que o autorizador não pode verificar se o token de acesso está vinculado ao certificado do cliente.
Ver "Entrada para um autorizador Lambda do Amazon API Gateway" para detalhes sobre como a escolha do tipo de carga de carga de eventos Lambda altera a entrada para o autorizador.
Se uma instância de AuthleteApi não é dado, Authorizer
‘construtor internamente cria um chamando AuthleteApiImpl(AuthleteEnvConfiguration())
e usa a instância para acessar APIs Authlete. Porque AuthleteEnvConfiguração usado lá assume que a configuração Authlete está disponível através de variáveis de ambiente, as seguintes variáveis de ambiente precisam ser definidas para a função Lambda que é usada como a implementação do seu autorizador Lambda.
Variável de Ambiente | Descrição |
---|---|
AUTHLETE_BASE_URL |
A URL base do servidor Authlete. |
AUTHLETE_SERVICE_APIKEY |
A chave API atribuída ao seu serviço. |
AUTHLETE_SERVICE_APISECRET |
O segredo da API atribuído ao seu serviço. |
Recomenda-se aumentar o valor do tempo limite da função Lambda a partir do valor padrão, pois a chamada para a API de introspecção da Authlete pode levar mais tempo dependendo de várias condições.
Como criar e carregar um pacote ZIP da função Lambda é explicado em "Atualização de uma função com dependências adicionais" em "Pacote de implantação AWS Lambda em Python".
Abaixo está um exemplo que cria e carrega um arquivo ZIP do autorizador Lambda com o authlete pacote.
~$ mkdir authorizer
~$ cd authorizer
~/authorizer$ vi lambda_function.py
~/authorizer$ pip install --target ./package authlete
~/authorizer$ (cd package; zip -r9 ../function.zip .)
~/authorizer$ zip -g function.zip lambda_function.py
~/authorizer$ aws lambda update-function-code --function-name authorizer --zip-file fileb://function.zip
Os testes podem ser a parte mais difícil neste tutorial, pois existem muitos passos para configurar o ambiente de teste ilustrado abaixo.
Vamos dar os seguintes passos um por um juntos.
Vamos usar java-oauth-servidor como servidor de autorização. É uma implementação amostra de servidor de autorização escrito em Java que usa o Authlete como um serviço backend.
$ git clone https://github.com/authlete/java-oauth-server
$ cd java-oauth-server
$ vi authlete.properties
$ docker-compose up
As linhas de comando acima iniciarão um servidor de autorização (java-oauth-server) em sua máquina local em http://localhost:8080
.
Em seguida, faça login Console do proprietário de serviço e configurar o serviço correspondente ao servidor de autorização para que ele possa suportar a vinculação do certificado.
Crie uma chave privada e, em seguida, um certificado auto-assinado para o servidor de autorização.
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 > server_private_key.pem
$ openssl req -x509 -key server_private_key.pem -subj /CN=localhost > server_certificate.pem
Note que openssl
comando usado neste tutorial é OpenSSL’s. Porque openssl
comando instalado no macOS é LibraSSL desde High Sierra, você tem que instalar OpenSSL’s openssl
para tentar linhas de comando neste tutorial como eles são.
$ /usr/bin/openssl version -a
LibreSSL 2.6.5
......
$ brew install openssl
$ /usr/local/opt/openssl/bin/openssl version -a
OpenSSL 1.1.1g 21 Apr 2020
......
Como o servidor java-oauth em si não protege seus pontos finais pelo TLS, um proxy reverso precisa ser colocado na frente do java-oauth-server para aceitar conexões TLS. Abaixo está um exemplo de arquivo de configuração que configura Nginx como um proxy reverso.
events {}
http {
server {
# Accept TLS connections at port 8443.
listen 8443 ssl;
# Server certificate in PEM format
ssl_certificate /path/to/server_certificate.pem;
# Servicer private key in PEM format
ssl_certificate_key /path/to/server_private_key.pem;
# Enable mutual TLS. 'optional_no_ca' requests a client certificate but
# does not require it to be signed by a trusted CA certificate. This is
# enough for this tutorial. See the document of ngx_http_ssl_module for
# details: http://nginx.org/en/docs/http/ngx_http_ssl_module.html
ssl_verify_client optional_no_ca;
# Pass the client certificate in the mutual TLS connection to the proxied
# server (java-oauth-server) as the value of 'X-Ssl-Cert' HTTP header.
# The proxied server has to be able to recognize the HTTP header, and
# java-oauth-server does recognize it. To be exact, authlete-java-jaxrs
# library used by java-oauth-server recognizes it.
proxy_set_header X-Ssl-Cert $ssl_client_escaped_cert;
# Pass requests that Nginx receives at 'https://localhost:8443/token' to
# 'http://localhost:8080/api/token' ('/api/token' of java-oauth-server).
# Note that TLS is terminated here.
location = /token {
proxy_pass http://localhost:8080/api/token;
}
}
}
Esta configuração faz com que o Nginx seja executado em https://localhost:8443
e encaminhar pedidos para /token
Para http://localhost:8080/api/token
.
Se o nome do arquivo de configuração for nginx.conf
, Nginx pode ser iniciado digitando o comando abaixo.
$ nginx -c $PWD/nginx.conf
Quando quiser parar o Nginx, digite isto:
$ nginx -s stop
Login Console de desenvolvedores e alterar a configuração de um aplicativo cliente que você vai usar para testes para habilitar a vinculação do certificado.
Crie uma chave privada e, em seguida, um certificado auto-assinado para a solicitação do cliente.
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 > client_private_key.pem
$ openssl req -x509 -key client_private_key.pem -subj /CN=client.example.com > client_certificate.pem
Crie mais um par de chaves privadas e certificado para usar para testes posteriormente. Certifique-se de especificar um valor diferente para o nome comum (para /CN=
), porque a configuração de domínio personalizada do Amazon API Gateway, que cobriremos na próxima seção, rejeita um arquivo truststore que contém certificados com o mesmo assunto.
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 > client_private_key_2.pem
$ openssl req -x509 -key client_private_key_2.pem -subj /CN=client2.example.com > client_certificate_2.pem
Para habilitar TLS mútuo no Amazon API Gateway, no momento desta escrita, você tem que atribuir um domínio personalizado (por exemplo, api.example.com
) para sua API.
O console Web do Amazon API Gateway fornece o menu “Nomes de domínio personalizados”. Para configurar um domínio personalizado lá sem problemas, é melhor preparar os seguintes itens com antecedência.
Um certificado de servidor para um domínio personalizado para o Amazon API Gateway deve estar sob gerenciamento de Gerente de Certificados AWS (ACM). Você pode importar certificados existentes ou criar novos em Console ACM. No entanto, os certificados importados não podem ser usados para domínios personalizados para o Amazon API Gateway quando o TLS mútuo estiver ativado. Então, crie um novo lá.
Quando você configurar tls mútuo, você será solicitado a inserir a localização de um arquivo que contém certificados de cliente confiáveis. O arquivo é chamado “loja de confiança”. A implementação do Amazon API Gateway mutual TLS verifica se o certificado de cliente apresentado durante o aperto de mão TLS está incluído na loja de confiança. Se não for incluído, o Amazon API Gateway rejeita a conexão sem invocar um autorizador Lambda.
Truststore é um arquivo de texto listando certificados de clientes em formato PEM como abaixo.
-----BEGIN CERTIFICATE-----
<Certificate contents>
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
<Certificate contents>
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
<Certificate contents>
-----END CERTIFICATE-----
...
Uma loja de confiança para este tutorial pode ser criada digitando os comandos abaixo.
$ cat client_certificate.pem > truststore.pem
$ cat client_certificate_2.pem >> truststore.pem
A loja de confiança precisa ser carregada para S3 para que o Amazon API Gateway possa se referir a ele.
$ aws s3 cp truststore.pem s3://{your-s3-bucket}
Os passos acima são descritos em "Configuração de autenticação TLS mútua para uma API REST". Consulte o documento para obter detalhes.
Agora você está pronto para registrar um domínio personalizado no Amazon API Gateway.
Habilitar Autenticação mútua de TLS na caixa “Detalhes do domínio” e um campo para Truststore URI aparecerá. Insira o S3 URI da sua loja de confiança no campo.
Em seguida, selecione o certificado de servidor para o domínio personalizado na caixa “Configuração de ponto final”.
Depois de registrar um domínio personalizado, você pode configurar como mapear “API” e “Stage” para “Path” sob o domínio personalizado. Na captura de tela abaixo, API “Exemplo” dev
palco é mapeado para o caminho do domínio personalizado dev
.
O passo final para a configuração personalizada de domínio é adicionar um registro CNAME ao seu servidor DNS para que o domínio personalizado possa ser roteado para “nome de domínio API Gateway”.
Fizemos toda a preparação. Vamos pegar um. token de acesso vinculado a certificados pelo fluxo de código de autorização.
No fluxo de código de autorização, o primeiro passo é enviar um solicitação de autorização para o endpoint de autorização do servidor de autorização através de um navegador web. Neste tutorial, o ponto final de autorização é http://localhost:8080/api/authorization
hospedado no java-oauth-server. Substituir ${CLIENT_ID}
na URL abaixo que representa uma solicitação de autorização com a ID do cliente real do seu aplicativo cliente e, em seguida, acessar a URL com o seu navegador web.
http://localhost:8080/api/authorization?response_type=code&client_id=${CLIENT_ID}&scope=profile+email&state=123
Seu navegador da Web exibirá um página de autorização gerado pelo servidor de autorização. Vai parecer lá embaixo.
A página tem campos de entrada para ID de login e senha. Entrada john
e john
lá e pressione o botão “Autorizar”, e seu navegador da Web será redirecionado para o ponto final de redirecionamento do aplicativo do seu cliente.
Se você não mudou redirecionar URI do seu aplicativo cliente a partir do valor padrão, a URL do ponto final de redirecionamento é https://{authlete-server}/api/mock/redirection/{service-api-key}
.
A URL do ponto final de redirecionamento que você pode ver na barra de endereço do seu navegador contém code
parâmetro de resposta como abaixo.
https://{authlete-server}/api/mock/redirection/{service-api-key}?state=123&code=RwRq2Lp0bJVMiLPKAFz4qB1hxieBD1X5HKuv8EPkJeM
O valor de code
parâmetro de resposta é o código de autorização que foi emitido do servidor de autorização para o aplicativo do seu cliente. O código de autorização é necessário quando o aplicativo do cliente faz uma solicitação de token.
Depois de obter um código de autorização, o aplicativo do cliente envia um solicitação de token para o ponto final token do servidor de autorização. Neste tutorial, o ponto final do token é https://localhost:8443/token
hospedado em Nginx.
Uma solicitação de token pode ser feita por curl
comando em um terminal shell. Abaixo está um exemplo de solicitação de token. Não se esqueça de substituir $CLIENT_ID
e $CODE
com o ID do cliente real e o código de autorização real antes de digitar.
$ curl -k --key client_private_key.pem --cert client_certificate.pem https://localhost:8443/token -d grant_type=authorization_code -d client_id=$CLIENT_ID -d code=$CODE
Argumento | Descrição |
---|---|
-k |
não verifique o certificado do servidor. Como este tutorial usa um certificado de servidor auto-assinado, essa opção é necessária. |
--key client_private_key.pem |
especifica a chave privada do cliente. |
--cert client_certificate.pem |
especifica o certificado do cliente. |
https://localhost:8443/token |
A URL do ponto final do token. |
-d grant_type=authorization_code |
indica que o fluxo é o fluxo de código de autorização. |
-d client_id=$CLIENT_ID |
especifica o ID do cliente. $CLIENT_ID com a ID do cliente real do seu aplicativo de cliente. |
-d code=$CODE |
especifica o código de autorização. Substituir $CODE com o código de autorização real. |
O ponto aqui é que é necessário passar a chave privada do cliente e certificado para curl
comando usando --key
opção e --cert
opção porque o ponto final do token requer TLS mútuo (ou seja, requer um certificado de cliente durante o aperto de mão TLS). O token de acesso emitido a partir do ponto final do token será vinculado ao certificado do cliente usado na solicitação de token.
Quando uma solicitação de token é bem sucedida, o ponto final do token retorna JSON que inclui access_token
propriedade como abaixo.
{
"access_token": "b5qgqkXpzObRyceBqKeGPDCT9NX9GGXSt_oSYBSj7GQ",
"refresh_token": "1iSpvpeznTzwdUJzwRbt-abqE4znWn_yhN5PbBKV9zw",
"scope": "email profile",
"token_type": "Bearer",
"expires_in": 86400
}
O valor do access_token
propriedade é o token de acesso emitido. O aplicativo do cliente usa o token de acesso quando faz chamadas de API.
Finalmente, ficamos prontos para acessar recursos (APIs) no Amazon API Gateway, que são protegidos por tokens de acesso vinculados a certificados.
Primeiro, acesse um recurso com o token de acesso e o certificado de cliente certo. Substituir ${ACCESS_TOKEN}
, ${CUSTOM_DOMAIN}
e ${RESOURCE}
no exemplo abaixo com valores reais de vocês. Se todas as coisas foram configuradas corretamente, você pode obter o recurso com sucesso sem ser bloqueado.
$ curl --key client_private_key.pem --cert client_certificate.pem -H "Authorization: Bearer ${ACCESS_TOKEN}" https://${CUSTOM_DOMAIN}/${RESOURCE}
Em seguida, acesse o recurso com o mesmo token de acesso e um certificado de cliente errado (client_certificate_2.pem
neste tutorial).
$ curl --key client_private_key_2.pem --cert client_certificate_2.pem -H "Authorization: Bearer ${ACCESS_TOKEN}" https://${CUSTOM_DOMAIN}/${RESOURCE}
Você receberá a seguinte resposta de erro.
{"Message":"User is not authorized to access this resource with an explicit deny"}
Isso inidique que o Amazon API Gateway rejeitou o acesso aos recursos. O ponto mais importante aqui é que o acesso ao recurso foi rejeitado embora o token de acesso fosse legítimo e foi porque o certificado de cliente apresentado em conjunto não era o legítimo vinculado ao token de acesso. Isso é vinculação de certificado.
Você completou este tutorial e agora pode proteger suas APIs no Amazon API Gateway com mais segurança do que nunca antes de utilizar a vinculação de certificados (RFC 8705)!
Entre em contato conosco se você precisar de apoio. Você é sempre bem-vindo!