Poppulo’s public cloud of choice is AWS, which provides a variety of methods to authorize access via its API Gateway. As our platform is built on a hybrid cloud, we've found that a Lamba Authorizer provides the most flexible approach to authorizing user actions.

To give a simple definition for this mechanism, it is a function which intercepts an incoming request, makes a decision, and returns a binary (yes/no) access policy. This loose specification is powerful, as it means authorization decisions can be made using information from a variety of sources, not necessarily bound to AWS. This scenario is common in hybrid architectures, where a company may have on-premise services or databases with information which has not yet been migrated to the cloud.

An overview of the authorization workflow can be seen here, as defined by AWS’ documentation. The Lambda Auth function shown in the image identifies the Lambda Authorizer:

An architectural diagram of an AWS Lambda Authorizer.
Source: AWS Documentation

In Poppulo's case, a client would send a bearer token issued by our Identity Provider, Curity. This bearer token references information about a person or machine, and allows the Lambda Authorizer to make an access decision.

As Lambda Authorizers support validation from any source, it is an excellent choice for organizations who have delegated Identity Management to a cloud-agnostic provider.

How It Works

A Lambda Authorizer receives the caller's identity in a bearer token, such as a JSON Web Token (JWT) or an OAuth token. You specify the name of a header, usually Authorization, used to validate your request. The value of this header is passed into your lambda in an event object, which looks like this:

{
  "methodArn":"arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]",
  "httpMethod": 'GET',
  "headers": { Authorization: '{caller-supplied-token}' }
}

This is passed to a Lambda Authorizer, shown here as a simple TypeScript function:

exports.handler = async (event) => {
    const token = event.headers.Authorization;
    
    // Validate token based on properties
    // relevant to your business.
    const result = validate(token);
    
    if (!result) {
    	return generatePolicy("Deny", event.methodArn);
    }
    
    return generatePolicy("Allow", event.methodArn);
};

In the Lambda Authorizer, an ALLOW or DENY decision is made and then passed back to the api gateway as a policy.

The output of this method is a policy document in JSON format:

{
	"principalId": "user",
	"policyDocument": {
		"Version": "2012-10-17",
		"Statement": [
			{
				"Action": "execute-api:Invoke",
				"Effect": "Allow",
				"Resource": "arn:aws:execute-api:us-east-1:account.../GET"
            }
		]
	}
}

To clarify the terms in this document, here's a breakdown:

  • Principal Id: The principalId is a required property on your Authorizer response. It represents the caller's identity.
  • Policy Document: The policyDocument is a required property and the core of the Authorizer response. You must return a valid IAM policy that allows access to the requested API Gateway resource. This requires defining an execution permission allowing the caller to perform a specific action, like execute-api:Invoke, and specifying the resource on which they are allowed to perform this action.

This, then, is all you need. An input event, a function that validates a token, and a policy as output.

First Steps

Having adopted CDK a few months ago, we're now big believers. From quickly scaffolding a project, to easily adopting TypeScript for Node.js lambdas, CDK has proven itself again and again. It should come as no surprise that we highly recommend this approach.

An overview of a CDK application.
Source: AWS Documentation

Start by installing CDK globally:

npm install -g aws-cdk

Then, create a new directory for your Lambda Authorizer, and initialize a git repository:

mkdir lambda-authorizer 
&& cd lambda-authorizer 
&& git init . 

Now we can create a simple CDK stack by initialising a project with a template – in this case, TypeScript:

cdk init app --language=typescript

Other language options are available too, depending on your preference :

Available templates:
* app: Template for a CDK Application
   └─ cdk init app --language=[csharp|fsharp|java|javascript|python|typescript]
* lib: Template for a CDK Construct Library
   └─ cdk init lib --language=typescript
* sample-app: Example CDK Application with some constructs
   └─ cdk init sample-app --language=[csharp|fsharp|java|javascript|python|typescript]

This will generate a very simple project scaffold, containing the bare minimum required to run what AWS calls a "Stack":

A simple bootstrapped CDK repo.
A simple bootstrapped CDK repo 

Next, we'll create a new folder in this repository called lambda, move into this directory via the command line, and initialise a new npm project. The -y flag here accepts all default config during creation:

mkdir lambda && cd lambda && npm init -y

Now we can create our TypeScript-based lambda, which CDK is configured to transpile automatically:

touch index.ts

In this file, add a handler, some validation logic, and a policy generator, similar to what we outlined earlier in this article:

exports.handler = async (event: any) => {
    var token = event.headers.Authorization;

    const result = await validate(token);

    if (!result) {
		return generatePolicy('Deny', event.methodArn);
    }

	return generatePolicy("Allow", event.methodArn);
};
A lambda handler
async function validate(token: string): Promise<boolean> {
    // Your validation logic here.
    return new Promise((resolve, reject) => {
        resolve(true);
    });
}
Your validation logic
interface AuthResponse {
    principalId: string;
    policyDocument: PolicyDocument;
}

interface PolicyDocument {
    Version: string;
    Statement: Statement[];
}

interface Statement {
    Action: string;
    Effect: string;
    Resource: string;
}

function generatePolicy(effect: string, resource: string) {
    const statement: Statement = {
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: resource,
    };

    const policyDocument: PolicyDocument = {
        Version: '2012-10-17',
        Statement: [statement],
    };

    const authResponse: AuthResponse = {
        principalId: 'user',
        policyDocument: policyDocument,
    };

    return authResponse;
}
The policy generator

Next, switch to the lib folder, and install the aws-lambda dependency:

npm install @aws-cdk/aws-lambda

Then, register your new lambda authorizer as part of the stack:

import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';

export class LambdaAuthorizerStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

		// Your new authorizer
        const authorizer = new lambda.Function(this, 'index', {
            runtime: lambda.Runtime.NODEJS_10_X,
            code: lambda.Code.fromAsset('lambda', { exclude: ['*.ts'] }),
            handler: 'index.handler',
        });
    }
}

Finally, build and deploy your stack to AWS.

npm run build && cdk deploy

This action will generate IAM Policy changes, to allow the execution of the lambda. Choose y to proceed:

A CDK warning message indicating sensitive security changes
A CDK warning message indicating sensitive security changes

Once complete, you can see the newly registered and currently updating stack in your AWS console, under the Cloudformation "Stacks" section.

This area provides an overview of all configuration and resource details attached to this stack definition:  

AWS' CloudFormation Stacks panel
AWS' CloudFormation Stacks panel

To ensure that our Lambda Authorizer was transpiled and uploaded correctly, click on the Resources tab. There you should see an item marked "Index" with some unique identifier beside it, and a type of AWS::Lambda::Function.  

These details identify our newly created Authorizer:

AWS' CloudFormation Stack Resources panel
AWS' CloudFormation Stack Resources panel

Click through the lambda's link, under the heading of Physical ID. This brings you to an AWS code environment, showing the transpiled contents of our Lambda Authorizer:

AWS' Lambda code environment
AWS' Lambda code environment

Congratulations, you've just built and deployed a type-safe, lightweight, version-controlled Lambda Authorizer to AWS! 🎉

Attaching It to a Gateway

Let's assume that our Lambda Authorizer will run in an account that we own and operate.

Go to the AWS API Gateway section in the AWS console, and choose an API that you wish to protect. Click on the Authorizers link, then click the blue button marked Create New Authorizer.

AWS' API Gateway Authorizer panel
AWS' API Gateway Authorizer panel

Choose a name, specify the Type as Lambda and then give it a reference to the Lambda you created earlier. This identifier is the Physical ID found in the CloudFormation resource definition.

Leave the "Lambda Invoke Role" section blank, as it will be auto-generated by AWS. Next, set the "Lambda Event Payload" as Request, the "Identity Source" as Authorization, and disable "Authorization Caching".


Heads-Up!

The default caching policy for newly created lambdas can be a security hazard. If you manage your identity, authentication, and authorization separately from AWS's native features, you must disable this. If not, you risk having two sources of truth for a user's allowed access period.

An authorizer with authorization caching enabled.
Disable, if you manage token lifecycles separately.

Once you've configured the Authorizer, hit Create. You will be prompted to add a permission to your newly created Lambda Authorizer Function, so that it may be invoked by the API Gateway.

Click Grant & Create to continue.

An automatic permission change prompt
An automatic permission change prompt

The above step creates a resource policy for you automatically, but it can also configured from the console if required, using the following templated command:

aws lambda add-permission  --function-name
"arn:aws:lambda:{region}:{ID}:function:{YourLambdaName}:live"
--source-arn "arn:aws:execute-api:{region}:{ID}:*"
--principal apigateway.amazonaws.com
--statement-id {YourIdentifier}
--action lambda:InvokeFunction

At this point, we've registered an API Gateway Authorizer, created an execution policy, and have all the individual components required to protect our APIs.

Finishing Touches

To assemble these components and protect an endpoint, we must start with a resource. Go to your API Gateway and select Resources. Identify a suitable resource to protect, then select an endpoint and its action, as seen here:

AWS' API Gateway Resource selection panel
AWS' API Gateway Resource selection panel

You should see a screen that looks similar to the following image. Identify the Method Request, which has the configuration Auth:NONE, and click on its heading.

AWS' API Gateway Resource Method Execution panel
AWS' API Gateway Resource Method Execution panel

This is the Method Request settings screen, which allows you to configure the endpoint with additional properties. Select Authorization, and choose Lambda Authorizer from the drop-down list, and confirm your choice.

It should look like this:

AWS' API Gateway Resource Method Request settings confirmation
AWS' API Gateway Resource Method Request settings confirmation

Finally, deploy your protected endpoint by selecting Deploy API from the resource Resource Actions menu.

AWS' API Gateway Deploy API Action
AWS' API Gateway Deploy API Action

Taking It for a Spin

Identify your API URL (shown just after deployment) and, using your client of choice, set an empty Authorization header, and make a GET request to that endpoint. This simulates a request from an unauthenticated actor.

As you can see from the image below, it will fail with 401 Unauthorized.

Postman showing an unauthorized request
Postman showing an unauthorized request

Repeat the above steps, but this time add a value for the Authorization header. This is your expected "happy path", where a user has a valid token after authenticating.

The request should succeed, with status 200 OK.

Success! 🎊

Postman showing an authorized request
Postman showing an authorized request

Takeaways

Lambda Authorizers are simple and scalable mechanisms for organisations looking to unify access to hybridised services through a cloud-based API.

For Poppulo, as we use a cloud-agnostic Identity Provider, AWS Lambda Authorizers ensure flexibility for the widest range of hybrid cloud configurations.

If you are on a similar path, we've found that the following recommendations have proven valuable:

  • Using CDK for lambda projects.
  • Using Request or Token type Lambda Authorizers, with bearer tokens.
  • Disabling policy caching in Lambda Authorizers if your identity and token time-to-live values are managed separately.

In addition, many of the steps outlined here follow an infrastructure-as-code pattern, but some intentionally do not, to help guide newcomers to AWS. In production we define all the above steps programmatically, and recommend that you do, too.