Create a Serverless App on AWS using TypeScript — Part 2
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.
Implementation
In terms of access control and securing our infrastructure, we’ll be implementing an architecture that looks like the following:
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.
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.
Next, we need to enter a Name and Identifier for our API
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
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:
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 Start
tab:
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:
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
- Getting started with the Serverless Framework
- Serverless Manifesto
- Serverless Framework vs Others
- Serverless architectures primer
- How we migrated our startup to serverless
- Why we switched from Docker to Serverless
- Serverless (FaaS) vs. Containers — when to pick which?
- Serverless Framework Guide to AWS
- Security best practices in IAM
- The ABCs of IAM: Managing permissions with Serverless
- Controlling and managing access to a REST API in API Gateway
- Amazon Cognito user pools
- What Is Amazon DynamoDB?
- Working with Tables and Data in DynamoDB
- Use API Gateway Lambda authorizers