DEV Community

Marcin Piczkowski
Marcin Piczkowski

Posted on

Serverless authorizers - custom REST authorizer

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.


This article was initially posted at https://cloudly.tech which is my blog about Serverless technologies and Serverless Framework in particular.

Top comments (10)

Collapse
 
hzburki profile image
Haseeb Burki

I used the provided code and it works when deployed as well. Now I'm removing the "Bearer" string from the token. I've removed the code that looks for "Bearer" string from the code and removed "indentityValidationExpression" from .yaml as well. But the authorizer still only works with the "Bearer" string in the Header.

Also I'm trying to change the response from "unauthorized" to anything else in the callback. But it still comes back "unauthorized".

Collapse
 
piczmar_0 profile image
Marcin Piczkowski

Are you sure you deployed full stack or single function only? Can you share your code on git?

Collapse
 
hzburki profile image
Haseeb Burki

I tried deploying just the authorizer as well as the whole stack. Still the same result. Turns out the authorizer in APIGW still have the "indentityValidationExpression" check set to Bearer (.*), even though I had removed it.

Thread Thread
 
piczmar_0 profile image
Marcin Piczkowski • Edited

To be completely sure your app is OK you can try to delete the stack and sls tmp folder called .serverless from your project root and redeploy from fresh. If this is the case maybe it's a bug in sls. You're using latest version, right?

Thread Thread
 
hzburki profile image
Haseeb Burki

yep I have the latest version .. I deleted the stack via "sls remove" but I'm still confused why the APIGW authorizer didn't update.

I'm still stuck at the authorizer, it times out or returns 500 whenever I try to match the token in my database. I'm using Sequelize and AWS RDS (MySQL). I can't give you my private repo, but I'll duplicate the code in a public repo.

It would be great if you could help! Thanks

Thread Thread
 
hzburki profile image
Haseeb Burki • Edited

github.com/hzburki/serverless

This is code repo. It's connected to a new database. Two routes /users and /user, an authorizer is connected to /user.

Works fine on serverless-offline, but both endpoints timeout when deployed to AWS. Even if I set timeout to 30sec.

Help Please !

Thread Thread
 
piczmar_0 profile image
Marcin Piczkowski

thx, will try to have a look at it by the end of this week..possibly sooner.

Thread Thread
 
piczmar_0 profile image
Marcin Piczkowski

I checked your code, added a couple of logs and changes.
I tested on AWS and it works.
You can check my code here: github.com/piczmar/sls-test-author...

I'm not sure what was your problem. I can think of wrong DB connection details causing Sequilize to wait on connection. Can you make sure the correct env. variables are set on Lambda function?

image

Can you check my version and see if it helped?

Thread Thread
 
hzburki profile image
Haseeb Burki

I got the authorizer to work :D

The issue was with the principalId. I wanted to set the authenticated object as the principalId and add it in the request body, that way I would save an extra database query. Once I set the principalId to the token. The authorizer started working.

I have to query the authenticated user again in my controller, but I can live with that.

Thanks for your help.

Thread Thread
 
piczmar_0 profile image
Marcin Piczkowski

Glad to hear that :)