Subscribe via RSS

Serverless APIs and User Authentication

With the advent of new services such as Amazon’s AWS Lambda and API Gateway, there is finally a path towards end-to-end API creation and service without having to ever spin up a server instance. Tech demos of the services have whet our appetite by showing the speed and flexibility of Lambda, and the ability to hook up to Internet accessible endpoints with API Gateway, even doing data lookups with DynamoDB. However, we never get a full workflow because they don’t talk about the most important part of an API: authentication of the user, especially through a federation service using a protocol like OAuth. In this article, we will show how we glue the aforementioned services together, and some additional tooling built to make deploying these services a bit easier.

The Parts

Like any good tech article, we will start with our tech stack:

Since we’re talking about “serverless” APIs, we will be using AWS Lambda as our main method of processing requests. Our Lambda functions will be written in Javascript (Node), one of several currently supported languages, with more on the way. For data persistence, AWS DynamoDB is simple and well supported. To expose our functions to the outside world, we will use AWS API Gateway to create a nice REST-ish URL structure. Finally, to act as our federated OAuth provider, we will authenticate against Google using their well-documented node libraries.

Setup

While building out the Lambda-backed authentication API, the biggest challenge we face is how to DRY out our code. Here we turn to the example given by the AWSM project. Any time something was shared between more than one function, we refactor it into its own npm module and required into each function’s index.js. Recent versions of npm allow us to keep these related components in the same repository through relative file path references – hurray! To keep the node_modules directory up-to-date, we also build a simple script which automates all the repetitive steps required to deploy each function. Stay tuned for more details in a future post.

Setting up the API gateway and getting it wired to the Lambda functions is left as an exercise to the reader, but here are a few tips:

  • Google’s OAuth requires the return URL it references be an HTTPS endpoint. If the protocol is wrong, it will error out with cryptic messages
  • API Gateway has some limitations to how it passes URL query parameters into the script. The following Velocity template will save you a ton of time:

      {
          "body" : $input.json('$'),
          "query": {#foreach($key in $input.params().querystring.keySet())#if($foreach.index > 0),#end"$key":"$input.params().querystring.get($key)"#end}
      }
    

Structure

Below is the layout for our authentication example with a simple user management layer.

.
├── .aws
├── .env
├── README.markdown
├── bin
│   └── deploy
├── lambda
│   └── users
│       ├── index
│       │   ├── index.js
│       │   └── package.json
│       └── login
│           ├── index.js
│           └── package.json
├── lib
│   ├── authenticate
│   │   ├── index.js
│   │   └── package.json
│   ├── token
│   │   ├── index.js
│   │   └── package.json
│   └── user
│       ├── index.js
│       └── package.json
└── package.json

The top level directories, bin, lambda, and lib are for project tooling, the actual lambda functions, and the common components shared between lambdas, respectively. As this is just a demonstration app, we only have the user login and index functions. Using the deploy script mentioned in the previous section, each lambda function is automatically detected within the lambda directory so they can be nested arbitrarily deep and structured to mimic their eventual API paths. At the top level there is a root package.json file which defines a project name and scripts. Finally, the .aws and .env files contain contain environment variables which are included at deploy- and runtime.

Authentication Flow

With our project skeleton in place, we can now perform the actual task of authenticating a user into our system. Our flow follows this ordering:

  • The requesting application does a GET to /users/login, which returns a URL which will prompt the user for their Google credentials;
  • On successful authentication, Google will redirect the user back to the same API endpoint with the addition of a temporary access code;
  • This code is used to request information about the user on the server-side, which is in turn used to look up or create their account and retrieve their access rights;
  • Finally, the authorized user is issued a JSON Web Token (JWT) they can use to access additional resources.
  • If a request to another endpoint is missing or has an invalid JWT, their request will be denied and they will have to authenticate again.

This flow is handled in a single Lambda function that uses shared libraries for authentication, users and tokens. Through liberal use of promises1, the core logic is contained in two functions that are very easy to follow:

// Our main entry point -- this just performs the first pre-auth step of our flow
function authInit(redirect) {
  return authenticate.getUrl(redirect)
  .then(function (url) {
    return {
      message: 'Authorization from Google required',
      url: url
    };
  });
}

// The meat of the flow -- this starts with a temp token from Google and ends by issuing a JWT
function authFinalize(code) {
  return authenticate.verifyCode(code)
  .then(authenticate.getUser)
  .then(extractEmails)
  .then(verifyDomain)
  .then(user.findOrCreateByEmail)
  .then(token.issue)
  .then(function(token) {
    return {
      message: 'Authorization Success!',
      jwt: token
    };
  });
}

1: AWS Lambda is running a fairly old version of Node and does not support native Promises. To get around this, we use the polyfill library bluebird, which also will promisify the AWS library functions.

Putting it All Together

Once your user is in possession of a JWT token, the sky is the limit. They may continue to make requests as an authenticated user until the JWT expires and they have to re-authenticate. It is then up to the API itself how to handle token renewal, revocation and other lifecycle tasks. The most difficult parts of getting this to work comes from getting all the pieces in place. Lambda is still very new and there are many improvements Amazon may still roll out, such as programmatic support of arbitrary headers from within a lambda function, but as an already heavily requested feature, it will at least be investigated.

To explore the nitty-gritty details of our authentication approach, we have open-sourced this example app. Go ahead and look around it here (key parts: auth and token shared libs) and if you have an improvement you would like to share, we love pull requests!

See Also

Has this article piqued your interest? Are you looking for more resources to help in your Lambda and API Gateway journeys? Be sure to check out these other great resources.

This project started out at one of our quarterly Hack Days, a full 24 hours where everyone comes together to work solo or in teams on something they have been itching to dig into. These projects are sometimes proofs of concept and other times come out polished enough to go into production within days or weeks. If this sounds interesting to you and you are looking to work with other creative and driven folks who love to explore new technologies and create great products that people love, we’re also hiring!