Securing APIs in Serverless (AWS Lambda)

Serverless is here to stay and there’s exponential growth in APIs written on Serverless. Securing APIs is a tricky topic and below article is an attempt at stitching together all needed concepts for securing API to API calls that handle both authentication and authorization.

Secure Cloud

From bird’s eye view — we are going to do below things

  • Develop two simple APIs on AWS Lamda using NodeJS
  • Configure Cognito pools
  • Call API 1 from API 2 using Cognito

Lets develop a simple API that gives back day of birth as well as age by taking date of birth as request parameter.

In order to demonstrate API to API calls — let’s assume API 1 will give day of birth and API 2 will give the age. Also assume that client will always invoke API 1.

In short Client → API 1 (Day of birth) → API 2 (age).

Response will come from API 1 to client and API 2 is opaque to client.

Let’s develop API 2 (Age Calculator)

Age calculator will be a straight forward API with below code

API response will be

{
"age": 31
}

Complete serverless code is available on Age Calculator repo — Github

Let’s develop API 1 (Day of Birth calculator)

Even day of birth API will be straight forward

API response will be

{
"day": "Friday"
}

Complete serverless code is available on Day of Birth repo — Github

So far..

We have developed 2 independent functions that perform 2 separate funcionalities — calculating age (API 2) and generating day of birth (API 1)

Now we will call API 2 from API 1 so that end user would see both day of birth and age from single API

For this, lets change API 1 a bit so that we make fetch call to Age Calculator

Complete code is at Day of Birth Repo — Github

Back to security

So far we have seen a simple API to API call without any security in place. We will now look into securing our second API (age calculator) so that only authorized users can call this API. For this we’ll depend on AWS Cognito

We will create User Pool where we can on board accounts and Identity Pools to get access

Creating User Pool

Login to AWS Console and select Cognito service and choose ‘Manage User Pools’

Name the user pool to some meaningful name — for our example, we name it as ‘security-example-pool’

Select email sign in as option (you can change as you want — this is for demo purpose)

and then choose options to similar to below (you can change as you want — this is for quick demo purpose)

Leave all defaults in next 2 pages ‘Do you want to customize your email verification messages’ and ‘Do you want to add tags for this user pool’ and ‘Do you want to remember your user’s devices’ ‘Which app clients will have access to this user pool’ ‘Do you want to customize workflows with triggers’

Finally you will end up with below screen

Select ‘Create Pool’ which will create user pool. Once created, select the newly created user pool and note down the Pool Id and Pool ARN which we will use later

Now we’ll add app client that has access to this pool. On pool details page, select ‘App Client’ and details similar to below

which will create details like below

Note down the App Client ID

Creating Identity Pool

In Cognito console, select ‘Manage Identity Pools’ and select ‘Create New Identity Pool’

User Pool ID and Client ID are from earlier User Pool exercise

On selecting ‘Create Pool’ you will be shown screen with roles like below

Now we have User Pool and Identity Pool ready

Creating user and adding to User Pool

In AWS CLI, add user using below

aws cognito-idp sign-up \
--region YOUR_COGNITO_REGION \
--client-id YOUR_COGNITO_APP_CLIENT_ID \
--username admin@example.com \
--password Passw0rd!

Eg:

aws cognito-idp sign-up --region us-east-1 --client-id 2su1g5iq748birpq5cscro8a45 --username test1@example.com --password Passw0rd!

Confirming signed up user

aws cognito-idp admin-confirm-sign-up \
--region YOUR_COGNITO_REGION \
--user-pool-id YOUR_COGNITO_USER_POOL_ID \
--username admin@example.com

Eg:

aws cognito-idp admin-confirm-sign-up --region us-east-1 --user-pool-id us-east-1_NfDyH1AB7 --username test1@example.com

If you get stuck on creating user pool and user — you can refer to clearer documentation at Serverless-Stack

Let’s add security to services

As discussed earlier, we would like to make age calculator secure so that only authorized apps can hit the API. Since we already deployed age calculator API, get the API’s resource ARN by navigating to API Gateway Consoleand selecting ‘dev-age-calc-service’ and select the path /age GET link

Resource ARN for this API will be similar to below

arn:aws:execute-api:us-east-1:900000005:p6q2ih1ryg/*/GET/age

Now using this ARN, let’s edit Identity Pool policy. Navigate to IAM console and select roles and search by prefix ‘Cognito_age_calc’ — this would show up 2 roles: poolAuth_Role and poolUnauth_Role

We are interested in Authenticated user’s role, so edit role’s policy with below (change IDs as needed)

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"mobileanalytics:PutEvents",
"cognito-sync:*",
"cognito-identity:*"
],
"Resource": [
"*"
]
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-east-1:900000005:p6q2ih1ryg/*/GET/age"
}
]
}

Back to code

Let’s enable security on age caclulator API — this is very simple using serverless framework. All we have to add is

authorizer: aws_iam

Entire function definition in serverless.yml is below

With that in place, once you deploy the function using ‘sls deploy’ and hit the request, you would notice this

{
"message": "Missing Authentication Token"
}

That’s awesome! We are now secure

Now time to hit API 2 from API 1 securely

In order to call API 2 from API 1, we need to make few changes on API 1’s side

In API 1 (day of birth service) terminal, do below installs

npm install amazon-cognito-identity-js aws-sdk crypto-js

and to API 1, add below 2 javascript files as utilities

SigV4Client.js

Security.js

Do note that Security.js expects below keys set in environment

COGNITO_REGION: us-east-1
COGNITO_USER_POOL_ID: us-east-1_SomeXYZ
COGNITO_IDENTITY_POOL_ID: us-east-1:553ad21e-f655-48ea-8444-SomePQR
COGNITO_APP_CLIENT_ID: 2su1g5iq748birpq5cscrsome
API_GATEWAY_REGION: us-east-1

In Serverless framework, recommended practice to set environments is in env.yml file. My env.yml looks like below

Day of Birth’s serverless.yml would look like below

Now let’s make change to original fetch call which was calling age calculator API with no security — below 2 methods are shown where calls are with security and no security

Notice that we are passing username and password first and getting token which later will be used to call API securely.

Bonus — I added my favorite ‘to’ method above that makes waiting on asynchronous calls neat to read

With all things in place, now it’s time to run the day of birth API — if everything is successful, you will see response similar to below

for GET — http://localhost:3000/day?dob=19880101

{
"day": "Friday",
"age": 31
}
That’s it.

Additional tips — instead of hard coding passwords, you can depend on AWS System Manager Parameter Store or AWS Secrets Manager (check costs)

Complete source code is available at

Credits — Serverless Stack