Mark Pollmann
BooksAbout

Writing Custom Authorizers for AWS API Gateway

5 minutes read

If you want to go serverless with your web app and you need an API running Lambda functions behind API Gateway on AWS is an excellent choice. The technology is mature, fast and cheap (if you know what you're doing). You even get the first 1 million invocations for free each month. Once it's set up you don't have to provision servers and worry about over- or underprovisioning; you just pay per API call.

Getting everything correctly configured is no small feat, though.

What are Custom Authorizers?

You probably don't want everyone to be able to call your REST-endpoint that fetches personal data from the database, the caller has to be authenticated. You could write this logic in the same Lambda function that handles the request but that could get messy very fast. Also you would have to duplicate this code for every endpoint and we don't want to repeat ourselves.

There are two types of Custom Authorizers, token-based and request-based. They differ in the way they grant the caller permission to use the resource, either they get a token back or

High-level Overview

The AWS docs provide this useful overview of the dataflow:

Custom Authorizer Workflow{:class="img-responsive"}

A simplistic round of steps

  1. Client sends a request to your API
  2. API Gateway extracts the token from the request and calls your custom authorizer with it
  3. Custom authorizer evaluates the token, generates a policy and sends it back to API Gateway.
  4. API Gateway evaluates the policy and calls your real lambda function that is registered for the API endpoint.

Hands-on

For our example we need three things:

  1. A lambda function that gets triggered when somebody calls our API Gateway endpoint.
  2. The actual API Gateway endpoint.
  3. A lambda function that serves as our custom authorizer.

Let's get started.

The Lambda Function

Let's log into AWS and create a new lambda function from scratch:

Custom Authorizer{:class="img-responsive"}

We give it just a basic execution role (to be able to write to CloudWatch) and use Node 6.10 as the runtime (still waiting for Node 8 and async/await).

We leave the code as is:

Custom Authorizer{:class="img-responsive"}

The API Gateway Endpoint

Now we go to API Gateway and set up a new API:

New API Gateway API{:class="img-responsive"}

In the next window, under actions we create a new resource under /test and enable CORS so we don't run into trouble by calling the API from our own machine:

New Resource{:class="img-responsive"}

After creating the resource we create a GET method (again under Actions) select Lambda integration type with Lambda proxy integration and select our lambda function we created in step one (remember the region you created the lambda function in):

Setup get endpoint{:class="img-responsive"}

Give API Gateway permission to execute your function in the next window and we're good to go.

Deploying the API

Again under Actions, we select deploy API, create a new stage and call it dev:

Deploy the API{:class="img-responsive"}

Now we have a deployed API. If you open all tabs until the GET endpoint you should find your exact url:

Deploy the API{:class="img-responsive"}

Let's try calling our (totally unauthenticated) endpoint with cURL:

Deploy the API{:class="img-responsive"}

It works! Now let's go about authenticating it:

The Custom Authenticator

We create a new lambda function as seen in step 1:

Deploy the API{:class="img-responsive"}

We leave the code as is for now.

The idea is that our function will:

  1. Get the token passed to it on the event object
  2. It does its authentication thing (validating the token)
  3. And then returns a policy document to API Gateway to explain if and what the caller is allowed to do.

To do step 1 we go back to API Gateway, select our API, then Authorizers and Create Authorizer

Deploy the API{:class="img-responsive"}

Here we can specify from which header API Gateway will extract the token and pass it to our authorizer. Usually the header should be called authorization or something like it but just to show you that you can use whatever you want we call it bananaHeader. Click create and go back to your GET /test method. Click on method request and under Authorization select your new authorizer:

Deploy the API{:class="img-responsive"}

Click the little checkmark and under Actions deploy the API again to stage dev.

Now when we call our /test endpoint our authenticator lambda function will run first. As of right now it just returns "Hello from lambda" which will of course authenticate nothing. Let's try calling the endpoint:

Deploy the API{:class="img-responsive"}

As expected, we're not getting through to our real endpoint. Let's write the custom authentication:

The Custom Authentication Code

In a real API your authentication code can get quite complex, calling services like Auth0 to see if the token is valid and not yet expired but let's keep it simple. The AWS docs have a great example for this. The token is a string and can either be 'allow', 'deny', 'unauthorized' or something else. Depending on which is the case they generate the corresponding policy document to tell API Gateway what the user is allowed to do.

Here's the code:

// A simple TOKEN authorizer example to demonstrate how to use an authorization token
// to allow or deny a request. In this example, the caller named 'user' is allowed to invoke
// a request if the client-supplied token value is 'allow'. The caller is not allowed to invoke
// the request if the token value is 'deny'. If the token value is 'Unauthorized', the function
// returns the 'Unauthorized' error with an HTTP status code of 401. For any other token value,
// the authorizer returns an 'Invalid token' error.

exports.handler = function(event, context, callback) {
  var token = event.authorizationToken
  switch (token.toLowerCase()) {
    case "allow":
      callback(null, generatePolicy("user", "Allow", event.methodArn))
      break
    case "deny":
      callback(null, generatePolicy("user", "Deny", event.methodArn))
      break
    case "unauthorized":
      callback("Unauthorized") // Return a 401 Unauthorized response
      break
    default:
      callback("Error: Invalid token")
  }
}

// Help function to generate an IAM policy
var generatePolicy = function(principalId, effect, resource) {
  var authResponse = {}

  authResponse.principalId = principalId
  if (effect && resource) {
    var policyDocument = {}
    policyDocument.Version = "2012-10-17"
    policyDocument.Statement = []
    var statementOne = {}
    statementOne.Action = "execute-api:Invoke"
    statementOne.Effect = effect
    statementOne.Resource = resource
    policyDocument.Statement[0] = statementOne
    authResponse.policyDocument = policyDocument
  }

  // Optional output with custom properties of the String, Number or Boolean type.
  authResponse.context = {
    stringKey: "stringval",
    numberKey: 123,
    booleanKey: true,
  }
  return authResponse
}

Let's use it in our lambda function and click save:

Deploy the API{:class="img-responsive"}

The Finish

This should be it!

If we call our API and provide a header called bananaHeader with value "allow" we should get back our hello from lambda.

Let's try it:

Deploy the API{:class="img-responsive"}

Looks good!

Conclusion

Authenticating endpoints in AWS can be quite a lot of configuration but once it's set up it's a cheap and easy way to get it done. If your method of authentication changes, because you switch providers for example, just update your lambda code and you're good to go.

Thanks for reading!