Amazon API Gateway + Custom Authorizer + OAuth

What is Custom Authorizer?

On Feb 11, 2016, a blog entry of AWS Compute Blog, “Introducing custom authorizers in Amazon API Gateway”, announced that Custom Authorizer had been introduced into Amazon API Gateway.

Thanks to this mechanism, an API built on Amazon API Gateway can delegate validation of a Bearer token (such as an OAuth or SAML token) presented by a client application to an external authorizer. The figure below is an excerpt from the online document “Enable Amazon API Gateway Custom Authorization” and “Lambda Auth function” at the top position in the figure is an authorizer. API Gateway delegates validation of a token to the authorizer if it is configured so.

Custom Auth Work Flow

As the same as before, Amazon API Gateway itself does not provide OAuth server functionalities, but you can protect APIs built on Amazon API Gateway by OAuth access tokens by utilizing Custom Authorizer.

No example

The online document and the blog show implementation examples of an authorizer. However, the example in the online document uses allow, deny and unauthorized as token values in order to simplify the code example, so it is not a practical example. On the other hand, the example in the blog uses JWT (RFC 7519) as a token value, so it is a practical example. But, it does not include any code to make a query to an external server to get information about a token. It is because JWT is a form where information is embedded in a token itself, and so information can be extracted only by decoding the token value.

That is, there is no example to communicate with an authorization server to get information about an access token. The lack of a code example for the use case is unfriendly to developers considering the following.

  • An authorizer has to be implemented as an AWS Lambda function.
  • As a language for AWS Lambda implementation, node.js is recommended more than others.
  • There is no standardized way to process network communication synchronously in node.js (AFAIK).

So, in the next section, we’ll show you an authorizer example written in node.js which communicates with an authorization server to get information about an access token.

Authorizer example

We show an authorizer example written in node.js which communicates with an external authorization server.

The introspection API used here is not the one defined in RFC 7662 (OAuth 2.0 Token Introspection) but Authlete’s introspection API. However, you can still get generic knowledge as to the following points.

  1. How to extract the HTTP method and the resource path of the request from the value of event.methodArn.
    (in extract_method_and_path function)
  2. How to extract an access token which is embedded in the form defined in RFC 6750, 2.1. from the value of event.authorizationToken.
    (in extract_access_token function)
  3. How to complete network communication with an authorization server synchronously in exports.handler using waterfall function of async module.
  4. How to communicate with an introspection API of an authorization server using request module.
    (in introspect function)
  5. How an authorizer generates a response to API Gateway.
    (in generate_policy function)
  6. How to set an HTTP status code to reject an request.
    (in exports.handler function)
// 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>

Create a lambda function deployment package

Here we show how to create a lambda function deployment package including the custom authorizer code above.

First, download index.js from Gist.

Then, open the file with a text editor and replace API_KEY and API_SECRET with actual values. Please use a pair of API credentials issued to you by Authlete. A pair of API credentials is issued when you sign up Authlete. Also, another pair is issued when you add a new service in Service Owern Console. See “Getting Started” for details.

Next, modify the implementation of get_required_scopes function as necessary. The role of the method is to return a list of necessary scopes based on the HTTP method and the resource path of a request. See the comment in index.js for details.

Then, move to the directory where index.js is placed and execute the following commands to install async module and requet module.

$ npm install async request

Operations so far have created index.js file and node_modules directory.

$ ls -1F
index.js
node_modules/

Finally, create a ZIP file containing these. The ZIP file is a lambda function deployment package. Upload it to AWS Lambda.

Note that it is recommended to set the timeout value of the lambda function longer than the default value because the Custom Authorizer implementation communicates with an external authorization server.

Configure Custom Authorizer

See the online document and the blog about how to use the uploaded lambda function as an implementation of Custom Authoriser.

Test

Here we assume that GET mydemoresource (which is created by going through the steps described in the Amazon API Gateway online document, “Walkthrough: Create API Gateway API for Lambda Functions”) is protected by the Custom Authorizer.

First, access mydemoresource without an access token. Don’t forget to replace {your-api-id} and {region-id} with your own.

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

You will receive “401 Unauthorized” when you execute the above command.

Next, access the API with an access token. Here we assume that puql0-wO_vwuxupctHgNem5-__b256tYgFcu_CXvc7w is a valid access token. (See the next section as to how to issue an access token.)

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

A successful response returns an HTTP status code “200 OK” and a JSON {"Hello":"World"}.

How to issue an access token

Authlete provides the default implementation of an authorization endpoint at the following URL:

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

The default implementation is called a direct endpoint and it is enabled by default. If you utilize the endpoint, you can issue an access token without using java-oauth-server.

The following URL is an example to get an access token issued using Implicit Flow. Don’t forget to replace {service-api-key} and {client-id} with your own.

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

Access the URL above by your browser, and an authorization page is displayed. Input the API key and the API secret of your service in the login form in the authorization page. After successful login, an access token is issued. See “Getting Started” for details.

Congratulations! You have succeeded in protecting APIs built on Amazon API Gateway by OAuth access tokens using Amazon API Gateway Custom Authorizer! Congratulations!