Serverless authorizers - custom REST authorizer

By Marcin Piczkowski Comment
Subscribe

In the series of articles I will explain basics of Servlerless authorizers in Serverless Framework: where they can be used and how to write custom authorizers for Amazon API Gateway. I am saying ‘authorizers’ but it is first of all about authentication mechanism. Authorization comes as second part.

Before we dive into details let’s think for a moment what kind of authentication techniques are available.

  • Basic

The most simple and very common is basic authentication where each request contains encoded username and password in request headers, e.g.:

GET /spec.html HTTP/1.1
Host: www.example.org
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
  • Token in HTTP headers

An example of this kind of authentication is OAuth 2. and JWT. The API client needs to first call sign-in endpoint (unsecured) with username and password in the payload to obtain a token. This token is later passed in headers of subsequent secured API calls. A good practice is to expire the token after some time and let the API client refresh it or sign in again to receive a new token.

GET /resource/1 HTTP/1.1
Host: example.com
Authorization: Bearer mF_9.B5f-4.1JqM
  • Query Authentication with additional signature parameters.

In this kind of authentication a signature string is generated from plain API call and added to the URL parameters. E.g. of such authentication is used by Amazon in AWS Signature Version 4

There are probably more variations of the above-mentioned techniques available, but you can get a general idea.

When to use which authentication mechanism?

The answer is as usual - it depends!

It depends if our application is a public REST API or maybe on-premises service which does not get exposed behind company virtual private network. Sometimes it’s also a balance between security and ease of use.

Let’s take e.g. Amazon Signature 4 signed requests. They are hard to create manually without using helpers API to sign requests (forget about Curl, which you could use easily with Basic and Token headers). On the other hand, Amazon explains that these requests are secured against replay attacks (see more here).

If you are building an API for banking then it must be very secure, but for most of the non-mission-critical cases, Token headers should be fine.

So we have chosen authentication and authorization mechanism. Now, how do we implement it with AWS?

We can do our own user identity storage or use an existing one, which is Amazon IAM ( Identity and Access Management ).

The last one has this advantage, that we don’t need to worry about secure storing of username and password in the database but rely on Amazon.

Custom REST Authorizer

Let’s first look at a simple example of REST API authorized with a custom authorizer

Create a new SLS project

serverless create --template aws-nodejs --path serverless-authorizers

Add simple endpoint /hello/rest

The code is here (Note the commit ID). The endpoint is completely insecure.

Deploy application

sls deploy -v function -f helloRest

When it deploys it will print endpoint URL, e.g.:

endpoints:
  GET - https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest

Call endpoint from client

Using curl we can call it like that:

curl https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest

Secure endpoint with custom authorizer.

For the sake of simplicity, we will only compare the token with a hardcoded value in authorizer function. In real case this value should be searched in the database. There should be another unsecured endpoint allowing to get the token value for username and password sent in the request.

Our authorizer will be defined in serverless.yml like this:

functions:
  authorizerUser:
    handler: authorizer.user
  helloRest:
    handler: helloRest.handler
    events:
      - http:
          path: hello/rest
          method: get
          authorizer: ${self:custom.authorizer.users}

custom:
  stage: ${opt:stage, self:provider.stage}
  authorizer:
    users:
      name: authorizerUser
      type: TOKEN
      identitySource: method.request.header.Authorization
      identityValidationExpression: Bearer (.*)

In http events section we defined authorizer as:

authorizer: ${self:custom.authorizer.users}

This will link to custom section where we defined authorizer with name authorizerUser. This is actually the name of a function which we defined in functions section as:

functions:
  authorizerUser:
    handler: authorizer.user

The handler points to a file where authorizer handler function is defined by naming convention: authorizer.user means file authoriser.js with exported user function.

The implementation will look as follows:


'use strict';

const generatePolicy = function(principalId, effect, resource) {
  const authResponse = {};
  authResponse.principalId = principalId;
  if (effect && resource) {
    const policyDocument = {};
    policyDocument.Version = '2012-10-17';
    policyDocument.Statement = [];
    const statementOne = {};
    statementOne.Action = 'execute-api:Invoke';
    statementOne.Effect = effect;
    statementOne.Resource = resource;
    policyDocument.Statement[0] = statementOne;
    authResponse.policyDocument = policyDocument;
  }
  return authResponse;
};

module.exports.user = (event, context, callback) => {

  // Get Token
  if (typeof event.authorizationToken === 'undefined') {
    if (process.env.DEBUG === 'true') {
      console.log('AUTH: No token');
    }
    callback('Unauthorized');
  }

  const split = event.authorizationToken.split('Bearer');
  if (split.length !== 2) {
    if (process.env.DEBUG === 'true') {
      console.log('AUTH: no token in Bearer');
    }
    callback('Unauthorized');
  }
  const token = split[1].trim();
  /*
   * extra custom authorization logic here: OAUTH, JWT ... etc
   * search token in database and check if valid
   * here for demo purpose we will just compare with hardcoded value
   */
   switch (token.toLowerCase()) {
    case "4674cc54-bd05-11e7-abc4-cec278b6b50a":
      callback(null, generatePolicy('user123', 'Allow', event.methodArn));
      break;
    case "4674cc54-bd05-11e7-abc4-cec278b6b50b":
      callback(null, generatePolicy('user123', 'Deny', event.methodArn));
      break;
    default:
      callback('Unauthorized');
   }

};

Authorizer function returns an Allow IAM policy on a specified method if the token value is 674cc54-bd05-11e7-abc4-cec278b6b50a. This permits a caller to invoke the specified method. The caller receives a 200 OK response. The authorizer function returns a Deny policy against the specified method if the authorization token is 4674cc54-bd05-11e7-abc4-cec278b6b50b. If there is no token in the header or unrecognized token, it exits with HTTP code 401 ‘Unauthorized’.

Here is the complete source code (note the commit ID).

We can now test the endpoint with Curl:

curl https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest

{"message":"Unauthorized"}

curl -H "Authorization:Bearer 4674cc54-bd05-11e7-abc4-cec278b6b50b" https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest

{"Message":"User is not authorized to access this resource with an explicit deny"}

curl -H "Authorization:Bearer 4674cc54-bd05-11e7-abc4-cec278b6b50a" https://28p4ur5tx8.execute-api.us-east-1.amazonaws.com/dev/hello/rest

{"message":"Hello REST, authenticated user: user123 !"}

More about custom authorizers in AWS docs

In the next series of Serverless Authorizers articles I will explain IAM Authorizer and how we can authorize GraphQL endpoints.


You can easily find the series articles with hashtag #authorizer

comments powered by Disqus