Secure the APIs
Now that we have created a User Pool, Identity Pool and an Auth Role; we are ready to use them to secure access to our APIs.
Serverless IAM Auth
Let’s start by replacing the functions:
block in our serverless.yml
.
functions:
# Defines an HTTP API endpoint that calls the main function in create.js
# - path: url path is /notes
# - method: POST request
# - authorizer: authenticate using the AWS IAM role
create:
handler: create.main
events:
- http:
path: notes
method: post
authorizer: aws_iam
get:
# Defines an HTTP API endpoint that calls the main function in get.js
# - path: url path is /notes/{id}
# - method: GET request
handler: get.main
events:
- http:
path: notes/{id}
method: get
authorizer: aws_iam
list:
# Defines an HTTP API endpoint that calls the main function in list.js
# - path: url path is /notes
# - method: GET request
handler: list.main
events:
- http:
path: notes
method: get
authorizer: aws_iam
update:
# Defines an HTTP API endpoint that calls the main function in update.js
# - path: url path is /notes/{id}
# - method: PUT request
handler: update.main
events:
- http:
path: notes/{id}
method: put
authorizer: aws_iam
delete:
# Defines an HTTP API endpoint that calls the main function in delete.js
# - path: url path is /notes/{id}
# - method: DELETE request
handler: delete.main
events:
- http:
path: notes/{id}
method: delete
authorizer: aws_iam
The key change here is the addition of the following line to each of our functions.
authorizer: aws_iam
This is telling Serverless Framework that our APIs are secured using an Identity Pool. Here is how it roughly works:
- A request with some signed authentication headers will be sent to our API.
- AWS will use the headers to figure out which Identity Pool is tied to it.
- The Identity Pool will ensure that the request is signed by somebody that has authenticated with our User Pool.
- If so, then it’ll assign the Auth IAM Role to this request.
- Finally, IAM will check to ensure that this role has access to our API.
If all goes well, your Lambda function will be invoked. And the event
parameter in your function handler will contain information about the user that called your API.
Cognito Identity Id
Recall the function signature of our Lambda functions:
export async function main(event, context) {}
Or the refactored one that we are now using:
export const main = handler(async (event, context) => {});
So far we’ve used the event
object to get the path parameters (event.pathParameters
) and request body (event.body
).
Now we’ll get the id of the authenticated user.
event.requestContext.identity.cognitoIdentityId;
This is an id that’s assigned to our user by our Cognito Identity Pool.
You’ll also recall that so far all of our APIs are hardcoded to interact with a single user (with user id 123
).
userId: "123", // The id of the author
Let’s change that.
Replace the above line in create.js
with.
userId: event.requestContext.identity.cognitoIdentityId, // The id of the author
Do the same in the get.js
.
userId: event.requestContext.identity.cognitoIdentityId, // The id of the author
And in the update.js
.
userId: event.requestContext.identity.cognitoIdentityId, // The id of the author
In delete.js
as well.
userId: event.requestContext.identity.cognitoIdentityId, // The id of the author
In list.js
find this line instead.
":userId": "123",
And replace it with.
":userId": event.requestContext.identity.cognitoIdentityId,
Keep in mind that the userId
above is the Federated Identity id (or Identity Pool user id). This is not the user id that is assigned in our User Pool. If you want to use the user’s User Pool user Id instead, have a look at the Mapping Cognito Identity Id and User Pool Id chapter.
Testing Locally
If you recall the chapters where we first created our API endpoints, we were using a set of mock events to test our Lambda functions. We stored these in the mocks/
directory.
For example, the create-event.json
looks like this.
{
"body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}"
}
Now we need to modify these to pass in the event.requestContext.identity.cognitoIdentityId
. Let’s now do that.
Replace the create-event.json
with this.
{
"body": "{\"content\":\"hello world\",\"attachment\":\"hello.jpg\"}",
"requestContext": {
"identity": {
"cognitoIdentityId": "USER-SUB-1234"
}
}
}
Here we are passing in a dummy value for the cognitoIdentityId
just for testing purposes.
So if you run the following in your project root.
$ serverless invoke local --function create --path mocks/create-event.json
You should see that a new note object has been created for our test user.
{
"statusCode": 200,
"body": "{\"userId\":\"USER-SUB-1234\",\"noteId\":\"0101be80-18b9-11eb-893d-b7fc3f6c5167\",\"content\":\"hello world\",\"attachment\":\"hello.jpg\",\"createdAt\":1603846842984}"
}
Let’s update our other mock events.
Replace the get-event.json
with this.
{
"pathParameters": {
"id": "cf6a83b0-1314-11eb-9506-9133509a950f"
},
"requestContext": {
"identity": {
"cognitoIdentityId": "USER-SUB-1234"
}
}
}
The update-event.json
with.
{
"body": "{\"content\":\"new world\",\"attachment\":\"new.jpg\"}",
"pathParameters": {
"id": "cf6a83b0-1314-11eb-9506-9133509a950f"
},
"requestContext": {
"identity": {
"cognitoIdentityId": "USER-SUB-1234"
}
}
}
And the delete-event.json
with.
{
"pathParameters": {
"id": "a63c5450-1274-11eb-81db-b9d1e2c85f15"
},
"requestContext": {
"identity": {
"cognitoIdentityId": "USER-SUB-1234"
}
}
}
Finally, the list-event.json
with.
{
"requestContext": {
"identity": {
"cognitoIdentityId": "USER-SUB-1234"
}
}
}
Now you can test your user connected Lambda functions locally.
Deploy the Changes
Let’s quickly deploy the changes we’ve made.
From your project root, run the following.
$ serverless deploy
Once deployed, you should see the deployed endpoints and functions.
Service Information
service: notes-api
stage: prod
region: us-east-1
stack: notes-api-prod
resources: 32
api keys:
None
endpoints:
POST - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes
GET - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id}
GET - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes
PUT - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id}
DELETE - https://0f7jby961h.execute-api.us-east-1.amazonaws.com/prod/notes/{id}
functions:
create: notes-api-prod-create
get: notes-api-prod-get
list: notes-api-prod-list
update: notes-api-prod-update
delete: notes-api-prod-delete
layers:
None
Next, let’s test our newly secured APIs.
For help and discussion
Comments on this chapter