Create a Serverless App on AWS using TypeScript — Part 2

Sidney Barrah
Level Up Coding
Published in
8 min readFeb 7, 2021

--

Photo by Philipp Katzenberger on Unsplash

Continuing on from where we stopped in the first part of how to create a simple serverless application, we will be focusing on how we implement authorization in API Gateway and ensure only authenticated requests can access our Lambda functions. To learn more about authorization mechanisms and established practices, see Controlling and Managing Access to a REST API in API Gateway. For this application, I have chosen to use a Lambda authorizer to control access to our Lambda functions created in the previous part of this article.

API Gateway Lambda Authorizers

API Gateway Lambda Authorizers are Lambda functions which are called by APIGateway to control access to our functions. With a Lambda authorizer, we can allow a request to continue to our API or reject the request as unauthorised. This means that we can control who can access our APIs.

One of the advantages of using a Lambda authorizer is then we can centralize our authentication logic in a single function instead of packaging in each individual function. This means that we are able to easily update and deploy our single auth function without needing to redeploy each function that uses authentication. Another advantage with using Lambda authorizers is that we are able to cache responses with auth logic that make remote calls. Making such remote calls can add unnecessary latency with every function running a check. Lambda authorizers also allow us to integrate third-party identity providers to control access to resources in API Gateway without having to configure services such as Amazon Cognito. It also gives us a flexibility in customising our authorization logic.

However, there are a few downsides with using Lambda authorizers which includes the added latency in API Gateway calls. AWS Lambda has a cold start problem and using a Lambda authorizer means we may have to pay the penalty twice — in our authorizer function and our core function. Also, without caching strategies we will have to deal with the additional network hop in our request flow. Another downside with using Lambda authorizers is that every endpoint which uses the authorizer must include the authorization information. This can prove to be inflexible for certain situations.

Lambda authorizer Auth workflow — AWS

Implementation

In terms of access control and securing our infrastructure, we’ll be implementing an architecture that looks like the following:

Authenticated API Architecture

Simply put, when a user makes a HTTP request to our API Gateway endpoint, the request must include the access token in the HTTP Authorization header. This is forwarded to the Lambda Authorizer function, which authenticates the token with the third-party identity provider. It then executes the authorization logic and returns an identity management policy to API Gateway. API Gateway uses this policy to evaluate if the user is allowed to invoke the API requested and either allows or denies the request. If allowed, API Gateway forwards the request to the Lambda function. You can find out more about how to use AWS Lambda authorizers with third party identity providers to secure API Gateway REST APIs.

Photo by Tolga Ulkan on Unsplash

Now for our application, we need to create a token-based Lambda authorizer using the third-party identity provider Auth0.

Sign up to Auth0 for free and on the Dashboard, click on APIs on the left sidebar menu and then the CREATE API button on the dashboard to setup the api for a new project.

Auth0 Dashboard

Next, we need to enter a Name and Identifier for our API

New API Form — Auth0

Click on the CREATE button to create a new API.

Next, in the API Quick Start tab, scroll down and make a note of the jwksUri

Auth0 API page

And also the issuer and audience values

We need to also get the Client ID and Client Secret from our API page:

Next, we need to add these values as environmental variables in our project. Modify the .env file and add the following:

// .envJWKS_URI="<JWKS-VA>"
AUDIENCE="<AUDIENCE-VALUE>"
TOKEN_ISSUER="<ISSUER-VALUE>"
AUTH0_CLIENT_ID="<CLIENT-ID>"
AUTH0_CLIENT_SECRET="<CLIENT-SECRET>"

Next, we need to create our authentication service. First, we install a few dependencies:

$ yarn add jsonwebtoken @types/jsonwebtoken jwks-rsa

Next, we create our auth.service.ts :

$ touch src/services/auth.service.ts

And update as follows:

The authenticate method above handles all the logic for our token authentication. First, it retrieves the token from the request using the _getToken method. This token is then decoded by jwt and then we retrieve the kid . This kid is then used to retrieve the Auth0 signing key using the _getSigningKey method. This key is verified and then we return the policy document.

We also need to create our Lambda authorizer function:

$ mkdir src/actions/auth
$ touch src/actions/auth/jwt-auth.action.ts

And update as follows:

This function calls our Auth service authenticate method. We wrap it around a try catch block so that any error is returned as an Unauthorized request.

To implement our new Lambda authorizer, we need to export our function in our handler.ts file:

// handler.ts// Custom API Gateway Authorizer
export { jwtAuth } from './src/actions/auth/jwt-auth.action';

And then update functions.ts :

// resources/functions.tsexport default {
jwtAuth: {
handler: 'handler.jwtAuth',
},
..........
}

To use the authorizer in a function, we need to update the http object by adding the authorizer property with a value of our authorizer:

http: {
method: '<METHOD>',
path: '<ENDPOINT-URI>',
authorizer: {
name: 'jwtAuth'
},
cors: true
}

Next, we update all our endpoints to use the authorizer so our endpoints will no longer accept requests without the right authorization token:

Sending a request to our endpoint without a valid authentication TOKEN should returned an Unauthorized response:

Postman — Failed Unauthorized request

We need to include a valid token in our request headers.

To obtain a valid token to test our endpoints, go to Application page in Auth0 and click on the Quick Starttab:

Scroll down to the Sending the token API section and copy the value of the header authorization property including Bearer

We need to use this Bearer token value in our request Authorization header:

Ensure there is a space between Bearer and the access_token . Click the Send button and we should receive a successful response:

Now, we have secure endpoints, we also need to update our feature tests.

We start by first installing dotenv so we can load environment variables from our .env file into process.env and use in our test handler

$ yarn add dotenv @types/dotenv

And then we update our package.json script :

"scripts": {
....
"feature-test": "./node_modules/.bin/mocha \"tests/feature/**/*.ts\" --require ts-node/register --require dotenv/config "
},

We need to also update our .env file with our BASE_URL variable:

BASE_URL=”http://localhost:3000/dev/"

Next, we update our test handler tests/lib/actions/handler:

Our post function starts by making a client credentials request to Auth0 and if successful, returns an access_token. With this access_token, we then make another request our endpoint. I have added an authorized parameter in the post function so we can also test Unauthorized requests to our APIs.

Next, we create a test for Unauthorized requests:

$ touch tests/feature/list/create-list/unauthorized.test.ts

And add our test:

Now we run our tests yarn run feature-test again:

Feature Tests

Next, we need to deploy our updated application to the default AWS profile by running the following in your working directory:

$ serverless deploy

Or to a specific profile by running:

$ serverless deploy --stage dev

You can refer to my article on how to create AWS credentials for local development and deployment. And that is it.

Conclusion

Serverless allows developers to quickly create and deploy microservice applications that scale on demand and are cheaper to run. This means that we gain efficiency; however, there are trade-offs in control and visibility. Serverless shortens development time, which means that we fail faster. Failing fast means we also learn faster.

Now that we have deployed our application, we can create a client-side application to use our APIs. However, I will not be covering that in this post.

This article aimed to demonstrate how to create and deploy a simple serverless application which uses DynamoDB, AWS Lambda and API Gateway. We have also learned how to manage and control access to our resources using API Gateway Lambda authorizers. We can also improve our application further by implementing a few features such as monitoring, continuous integration and deployment using platforms such as TravisCI, CircleCI or ConcourseCI and even adding a custom domain using a service such as Amazon Route 53. AWS also provides Cloudwatch, which monitors the resources and applications we run on the platform in real time.

I hope this article helps in your understanding of how to get started creating simple serverless applications and managing control to these services. I also hope it inspires you in some way as we continue to push the limits of what is possible in designing solutions for the future.

I have included a link to the GitHub repository below. Feel free to clone, fork or star it if it helps you:

Further Reading

--

--