Amazon API Gateway + Custom Authorizer + OAuth

O que é autorizador personalizado?

Em 11 de fevereiro de 2016, uma entrada do blog AWS Compute Blog, “Apresentando autorizadores personalizados no Amazon API Gateway”, anunciou que ** Autorizador Personalizado ** foi introduzido no Amazon API Gateway.

Graças a esse mecanismo, uma API construída no Amazon API Gateway pode delegar a validação de um token do Bearer (como um token OAuth ou SAML) apresentado por um aplicativo cliente a um autorizador externo. A figura abaixo é um trecho do documento online “[Habilitar autorização personalizada do Amazon API Gateway] [usar]” e “Função Lambda Auth” na posição superior da figura é um autorizador. O API Gateway delega a validação de um token ao autorizador, se estiver configurado dessa forma.

Custom Auth Work Flow

Como antes, o Amazon API Gateway em si não fornece funcionalidades de servidor OAuth, mas você pode proteger APIs construídas no Amazon API Gateway por tokens de acesso OAuth utilizando o Autorizador Personalizado.

Neste documento, usamos o termo “Autorizador Customizado”, que foi renomeado como “Autorizador Lambda”.

Antes da introdução do Autorizador Personalizado, a introspecção e a validação de um token de acesso tinham que ser executadas em uma implementação de uma função lambda para proteger APIs por tokens de acesso OAuth. Nosso documento “Amazon API Gateway + AWS Lambda + OAuth” mostra como fazer isso da maneira antiga.

Sem exemplo

O documento online e o blog mostram exemplos de implementação de um autorizador. No entanto, o exemplo no documento online usa allow,deny e unauthorized como valores de token para simplificar o exemplo de código, portanto, não é um exemplo prático. Por outro lado, o exemplo no blog usa JWT (RFC 7519) como um valor de token, por isso é um exemplo prático. Porém, não inclui nenhum código para fazer uma consulta a um servidor externo para obter informações sobre um token. Isso ocorre porque o JWT é um formulário em que as informações são incorporadas em um token e, portanto, as informações podem ser extraídas apenas pela decodificação do valor do token.

Ou seja, não há exemplo para se comunicar com um servidor de autorização para obter informações sobre um token de acesso. A falta de um exemplo de código para o caso de uso não é amigável para os desenvolvedores, considerando o seguinte.

  • Um autorizador deve ser implementado como uma função AWS Lambda.
  • Como linguagem para implementação do AWS Lambda, node.js é mais recomendado do que outros.
  • Não há uma maneira padronizada de processar a comunicação de rede de forma síncrona em node.js (AFAIK).

Portanto, na próxima seção, mostraremos um exemplo de autorizador escrito em node.js que se comunica com um servidor de autorização para obter informações sobre um token de acesso.

Exemplo de autorizador

Mostramos um exemplo de autorizador escrito em node.js que se comunica com um servidor de autorização externo.

A introspecção API usada aqui não é aquela definida em RFC 7662 (OAuth 2.0 Token Introspection), mas Authlete’s introspection API. No entanto, você ainda pode obter conhecimento genérico quanto aos seguintes pontos.

  1. Como extrair o método HTTP e o caminho do recurso da solicitação do valor de event.methodArn.
    (na funçãoextract_method_and_path)
  2. Como extrair um token de acesso que está embutido no formulário definido em RFC 6750, 2.1. do valor de event.authorizationToken.
    (na função extract_access_token)
  3. Como completar a comunicação de rede com um servidor de autorização de forma síncrona em exports.handler usando a função cascata do módulo async.
  4. Como se comunicar com uma API de introspecção de um servidor de autorização usando o módulo request.
    (na função introspect)
  5. Como um autorizador gera uma resposta para o Gateway API.
    (na função generate_policy)
  6. Como definir um código de status HTTP para rejeitar uma solicitação.
    (na função exports.handler)
// The API credentials of your service issued by Authlete.
// These are needed to call Authlete's introspection API.
var API_KEY    = '{Your-Service-API-Key}';
var API_SECRET = '{Your-Service-API-Secret}';

// Regular expression to extract an access token from
// Authorization header.
var BEARER_TOKEN_PATTERN = /^Bearer[ ]+([^ ]+)[ ]*$/i;

// Modules.
var async   = require('async');
var request = require('request');


// A function to extract the HTTP method and the resource path
// from event.methodArn.
function extract_method_and_path(arn)
{
  // The value of 'arn' follows the format shown below.
  //
  //   arn:aws:execute-api:<regionid>:<accountid>:<apiid>/<stage>/<method>/<resourcepath>"
  //
  // See 'Enable Amazon API Gateway Custom Authorization' for details.
  //
  //   http://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html
  //

  // Check if the value of 'arn' is available just in case.
  if (!arn)
  {
    // HTTP method and a resource path are not available.
    return [ null, null ];
  }

  var arn_elements      = arn.split(':', 6);
  var resource_elements = arn_elements[5].split('/', 4);
  var http_method       = resource_elements[2];
  var resource_path     = resource_elements[3];

  // Return the HTTP method and the resource path as a string array.
  return [ http_method, resource_path ];
}


// A function to extract an access token from Authorization header.
//
// This function assumes the value complies with the format described
// in "RFC 6750, 2.1. Authorization Request Header Field". For example,
// if "Bearer 123" is given to this function, "123" is returned.
function extract_access_token(authorization)
{
  // If the value of Authorization header is not available.
  if (!authorization)
  {
    // No access token.
    return null;
  }

  // Check if it matches the pattern "Bearer {access-token}".
  var result = BEARER_TOKEN_PATTERN.exec(authorization);

  // If the Authorization header does not match the pattern.
  if (!result)
  {
    // No access token.
    return null;
  }

  // Return the access token.
  return result[1];
}


// A function to get a list of required scopes as a string array
// from a combination of an HTTP method and a resource path.
// For example, ["profile", "email"]. When a non-empty array is
// returned, the Authlete server (= the implementation of Authlete's
// introspection API) checks if all the scopes are covered by the
// access token. When this method returns null, such a check on
// scopes is not performed.
function get_required_scopes(http_method, resource_path)
{
  // Customize as necessary.
  return null;
}


// A function to call Authlete's introspection API.
//
// This function is used as a task for 'waterfall' method of 'async' module.
// See https://github.com/caolan/async#user-content-waterfalltasks-callback
// for details about 'waterfall' method.
//
//   * access_token (string) [REQUIRED]
//       An access token whose information you want to get.
//
//   * scopes (string array) [OPTIONAL]
//       Scopes that should be covered by the access token. If the scopes
//       are not covered by the access token, the value of 'action' in the
//       response from Authlete's introspection API is 'FORBIDDEN'.
//
//   * callback
//       A callback function that 'waterfall' of 'async' module passes to
//       a task function.
//
function introspect(access_token, scopes, callback)
{
  request({
    // The URL of Authlete's introspection API.
    url: 'https://api.authlete.com/api/auth/introspection',

    // HTTP method.
    method: 'POST',

    // The API credentials for Basic Authentication.
    auth: {
      username: API_KEY,
      pass: API_SECRET
    },

    // Request parameters passed to Authlete's introspection API.
    json: true,
    body: {
      token: access_token,
      scopes: scopes
    },

    // Interpret the response from Authlete's introspection API as a UTF-8 string.
    encoding: 'utf8'
  }, function(error, response, body) {
    if (error) {
      // Failed to call Authlete's introspection API.
      callback(error);
    }
    else if (response.statusCode != 200) {
      // The response from Authlete's introspection API indicates something wrong
      // has happened.
      callback(response);
    }
    else {
      // Call the next task of 'waterfall'.
      //
      // 'body' is already a JSON object. This has been done by 'request' module.
      // As for properties that the JSON object has, see the JavaDoc of
      // com.authlete.common.dto.IntrospectionResponse class in authlete-java-common.
      //
      //   http://authlete.github.io/authlete-java-common/com/authlete/common/dto/IntrospectionResponse.html
      //
      callback(null, body);
    }
  });
}


// A function to generate a response from Authorizer to API Gateway.
function generate_policy(principal_id, effect, resource)
{
  return {
    principalId: principal_id,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: resource
      }]
    }
  };
}


// An authorizer implementation
exports.handler = function(event, context)
{
  // Get information about the function that is requested to be invoked.
  // Extract the HTTP method and the resource path from event.methodArn.
  var elements = extract_method_and_path(event.methodArn);
  var http_method   = elements[0];
  var resource_path = elements[1];

  // The access token presented by the client application.
  var access_token = extract_access_token(event.authorizationToken);

  // If the request from the client does not contain an access token.
  if (!access_token) {
    // Write a log message and tell API Gateway to return "401 Unauthorized".
    console.log("[" + http_method + "] " + resource_path + " -> No access token.");
    context.fail("Unauthorized");
    return;
  }

  // Get the list of required scopes for the combination of the HTTP method
  // and the resource path.
  var required_scopes = get_required_scopes(http_method, resource_path);

  async.waterfall([
    function(callback) {
      // Validate the access token by calling Authlete's introspection API.
      introspect(access_token, required_scopes, callback);
    },
    function(response, callback) {
      // Write a log message about the result of the access token validation.
      console.log("[" + http_method + "] " + resource_path + " -> " +
                  response.action + ":" + response.resultMessage);

      // The 'action' property contained in a response from Authlete's
      // introspection API indicates the HTTP status that the caller
      // (= an implementation of protected resource endpoint) should
      // return to the client application. Therefore, dispatch based
      // on the value of 'action'.
      switch (response.action) {
        case 'OK':
          // The access token is valid. Tell API Gateway that the access
          // to the resource is allowed. The value of 'subject' property
          // contained in a response from Authlete's introspection API is
          // the subject (= unique identifier) of the user who is associated
          // with the access token.
          context.succeed(generate_policy(response.subject, 'Allow', event.methodArn));
          break;

        case 'BAD_REQUEST':
        case 'FORBIDDEN':
          // Tell API Gateway that the access to the resource should be denined.
          context.succeed(generate_policy(response.subject, 'Deny', event.methodArn));
          break;

        case 'UNAUTHORIZED':
          // Tell API Gateway to return "401 Unauthorized" to the client application.
          context.fail("Unauthorized");
          break;

        case 'INTERNAL_SERVER_ERROR':
        default:
          // Return "Internal Server Error". When the value passed to
          // context.fail() is other value than "unauthorized", it is
          // treated as "500 Internal Server Error".
          context.fail("Internal Server Error");
          break;
      }

      callback(null);
    }
  ], function (error) {
    if (error) {
      // Something wrong has happened.
      context.fail(error);
    }
  });
};
  </resourcepath></method></stage></apiid></accountid></regionid>

Crie um pacote de implantação de função lambda

Aqui, mostramos como criar um pacote de implantação de função lambda incluindo o código do autorizador personalizado acima.

Primeiro, baixe index.js do Gist.

Em seguida, abra o arquivo com um editor de texto e substitua API_KEY eAPI_SECRET pelos valores reais. Use um par de credenciais de API emitidas para você pela Authlete. Um par de credenciais de API é emitido quando você se inscreve no Authlete. Além disso, outro par é emitido quando você adiciona um novo serviço no Service Owern Console. Consulte “Introdução” para obter detalhes.

Em seguida, modifique a implementação da função get_required_scopes conforme necessário. A função do método é retornar uma lista de escopos necessários com base no método HTTP e no caminho do recurso de uma solicitação. Veja o comentário em index.js para detalhes.

Em seguida, vá para o diretório onde index.js está colocado e execute os seguintes comandos para instalar o módulo assíncrono e o módulo de requet.

$ npm install async request

As operações até agora criaram o arquivo index.js e o diretórionode_modules.

$ ls -1F
index.js
node_modules/

Finalmente, crie um arquivo ZIP contendo estes. O arquivo ZIP é um pacote de implantação de função lambda. Faça upload para o AWS Lambda.

Observe que é recomendado definir o valor de tempo limite da função lambda por mais tempo do que o valor padrão porque a implementação do Autorizador Customizado se comunica com um servidor de autorização externo.

Configurar autorizador personalizado

Veja [o documento online] [uso] e o blog sobre como usar a função lambda carregada como uma implementação do Autorizador Personalizado.

Teste

Aqui, assumimos que GET mydemoresource (que é criado seguindo as etapas descritas no documento online do Amazon API Gateway, “Passo a passo: Criar API Gateway de API para funções Lambda”) é protegido pelo Autorizador Customizado.

Primeiro, acesse mydemoresource sem um token de acesso. Não se esqueça de substituir {your-api-id} e {region-id} com o seu próprio.

curl -v -s -k https://{your-api-id}.execute-api.{region-id}.amazonaws.com/test/mydemoresource

Você receberá “401 Unauthorized” ao executar o comando acima.

Em seguida, acesse a API com um token de acesso. Aqui, assumimos que puql0-wO_vwuxupctHgNem5 -__ b256tYgFcu_CXvc7w é um token de acesso válido. (Consulte a próxima seção sobre como emitir um token de acesso.)

curl -v -s -k https://{your-api-id}.execute-api.{region-id}.amazonaws.com/test/mydemoresource \
     -H 'Authorization: Bearer puql0-wO_vwuxupctHgNem5-__b256tYgFcu_CXvc7w'

Uma resposta bem-sucedida retorna um código de status HTTP “200 OK” e um JSON {" Hello ":" World "}.

Como emitir um token de acesso

O Authlete fornece a implementação padrão de um endpoint de autorização no seguinte URL:

https://api.authlete.com/api/auth/authorization/direct/{service-api-key}

A implementação padrão é chamada de terminal direto e é habilitada por padrão. Se você utilizar o endpoint, poderá emitir um token de acesso sem usar java-oauth-server.

O URL a seguir é um exemplo para obter um token de acesso emitido usando Implicit Flow. Não se esqueça de substituir {service-api-key} e {client-id} pelo seu próprio.

https://api.authlete.com/api/auth/authorization/direct/{service-api-key}?client_id={client-id}&response_type=token

Acesse o URL acima pelo seu navegador, e uma página de autorização é exibida. Insira a chave da API e o segredo da API do seu serviço no formulário de login na página de autorização. Após o login bem-sucedido, um token de acesso é emitido. Consulte “Introdução” para obter detalhes.

Parabéns! Você conseguiu proteger APIs construídas no Amazon API Gateway por tokens de acesso OAuth usando o Amazon API Gateway Custom Authorizer! Parabéns!