javascript

Secure Your AWS Lambdas with TypeScript

Camilo Reyes

Camilo Reyes on

Secure Your AWS Lambdas with TypeScript

In the previous part of this series, we optimized our Lambda function. However, our API is open to the public — anyone with the URL can use it and get a response.

In this take, we are going to secure our API using a tool called Amazon Cognito. This will only allow authenticated users access to our endpoints.

Ready? Let’s go!

What Is Cognito?

The AWS cloud has Amazon Cognito, an identity and access management tool. With this, you can create user accounts, manage their credentials, and generate a JWT.

What Is a JWT?

Think of a JWT as an identity card. It identifies you as a real person and grants you basic access to a resource like an endpoint.

JWT stands for JSON Web Token and allows for two parties, like a client and server, to exchange claims. The JWT gets signed using a cryptographic algorithm to ensure that claims cannot be altered after the token is generated. A JWT is also self-contained, meaning it does not depend on database queries or a server session to grant authentication.

We can secure our API with a bearer JWT token. This way, only authenticated users with valid credentials can use our API.

Let’s see how we can set up Amazon Cognito and generate a JWT.

Set Up Amazon Cognito

First, you need to create a user pool on the AWS cloud. Go to AWS and look for Cognito.

Follow these steps:

  • Click Create user pool
  • Click Username and Allow users to sign in with a preferred username
  • Go to Next
  • Disable MFA, so click No MFA
  • Disable Enable self-service account recovery
  • Go to Next
  • Disable Self-registration
  • Disable Cognito-assisted verification and confirmation
  • Skip past Required attributes and Custom attributes
  • Go to Next
  • On Email, click Send email with Cognito
  • Go to Next
  • Under User pool name, type pizza-api-cognito and make note of this name
  • On Initial app client, select Other for a custom app
  • Under App client name, type pizza-api-client
  • Click Generate a client secret
  • Expand Advanced app client settings and select ALLOW_USER_PASSWORD_AUTH authentication flow
  • Disable Token revocation and User existence errors
  • Go to Next
  • Click Create user pool

This may feel like a lot of steps to set up a user pool, but you’re really doing two things:

  1. Allowing the username and password auth flow.
  2. Creating a new app client.

MFA, self-service, and token revocation are outside the scope of this article, so we have these options disabled in Amazon Cognito.

Once these steps are complete, note the user pool ID, then click on your user pool.

Set a Username and Password

Now, under Users, click on Create user. Set the username to regular and make a note of it. Go ahead and click on Generate a password so that we can set the password later.

Then, click on Create user.

Note that the confirmation status says Force change password. This is because the password is currently unconfirmed.

To set a new password, run this AWS CLI command:

> aws cognito-idp admin-set-user-password --user-pool-id USER_POOL_ID --username regular --password Passw0rd! --permanent

The password has the following requirements:

  • A minimum length of 8 characters
  • Contains at least 1 number
  • Contains at least 1 special character
  • Contains at least 1 lowercase character

Be sure to set a more secure password and use that instead of this very insecure password. Replace USER_POOL_ID with the user pool ID from earlier.

Go back to AWS and check that the password is now Confirmed. We now have a legit password to use in the credentials.

Create a Client Secret Hash for AWS Cognito

Next, we’ll need a script that generates a secret hash before generating a JWT. If you do not provide a proper hash, you will see an error because Amazon Cognito cannot verify the secret hash.

Create a file — say, generate-client-hash.js — and write this algorithm:

const crypto = require("crypto");
 
HMAC_ALGORITHM = "sha256";
HMAC_FORMAT = "hex";
HASH_ENCODING = "base64";
 
const arguments = process.argv;
 
const username = arguments[2];
const clientId = arguments[3];
const clientSecret = arguments[4];
 
const hmac = crypto.createHmac(HMAC_ALGORITHM, clientSecret);
const data = hmac.update(username + clientId);
const genHmac = data.digest(HMAC_FORMAT);
const buff = Buffer.from(genHmac, HMAC_FORMAT);
const hash = buff.toString(HASH_ENCODING);
 
console.log("secret hash: " + hash);

The algorithm simply creates a base64 encoded hash from the username, client ID, and client secret.

To nab the client ID and secret, go to AWS and then Amazon Cognito, click on your user pool, and click App integration. The pizza-api-client should be at the bottom of the page with the client ID. To reveal the client secret, go to your client, then click Show client secret.

To generate a secret hash, run this command:

> node generate-client-hash.js regular CLIENT_ID CLIENT_SECRET

Make a note of this secret hash before continuing to the next section.

Generate a JWT

Phew, with Amazon Cognito out of the way, this is where the real fun begins.

Simply gather all your necessary inputs: client ID, password, and secret hash, and then do this:

> aws cognito-idp initiate-auth --auth-flow USER_PASSWORD_AUTH --client-id CLIENT_ID --auth-parameters USERNAME=regular,PASSWORD=PASSWORD,SECRET_HASH=SECRET_HASH

Note the IdToken in the result — this is the bearer JWT token you’ll use for authentication.

Let’s take a quick pause and inspect the token using jwt.io. Copy-paste the JWT into the Debugger to see what’s in the token.

{
  "sub": "26412b5f-7752-4176-a3de-1922b4690097",
  "aud": "4nfibi3q3jivsdoo38save91ku",
  "event_id": "46580b57-dabc-47e3-a4be-6461ab6d1c40",
  "token_use": "id",
  "auth_time": 1665342105,
  "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_1BfH4Jwkp",
  "cognito:username": "regular",
  "exp": 1665345705,
  "iat": 1665342105
}

Because the JWT is also signed by Amazon Cognito, it is impossible to brute force an attack on our endpoint with the bare claims. The beauty of this is that it is self-contained and has sufficient information in the claims to authenticate a real person.

For example:

  • sub uniquely identifies our Amazon Cognito user
  • token_use claims this is an ID token
  • exp sets an absolute expiration.

So, even if somebody somehow gets a hold of this token, it will long have expired by the time this article is published. In Amazon Cognito, the ID token expires an hour after it is generated.

Update the API Gateway in TypeScript

Finally, open up api.ts in the pizza-api code repo. We’ll need to make two changes: register an authorizer, and assign the authorizer to the endpoint.

Do this:

api.registerAuthorizer("pizza-api-cognito", {
  providerARNs: ["USER_POOL_ARN"],
});
 
api.get("/", () => ["Welcome to AWS"], {
  cognitoAuthorizer: "pizza-api-cognito",
});

The USER_POOL_ARN is a unique identifier you can find on AWS. Simply click on the user pool, and copy-paste the ARN in User pool overview.

With these changes done, it’s time to run npm run update and let Claudia do the rest of the work.

To verify that changes have been made on the API gateway:

  1. Go to AWS, and click API Gateway.
  2. Open up pizza-api and inspect the Authorizers. There should be a pizza-api-cognito authorizer.
  3. Check the API endpoints under Resources. The main / endpoint should have COGNITO_USER_POOLS assigned to the endpoint.

There is the option to manually do all this work on the API gateway, but the Claudia deploy tool automates all these steps for us.

Test It Out!

Fire up CURL to verify that the endpoint is no longer open to the public:

curl -i -H "Accept: application/json" "https://API_GATEWAY_ID.execute-api.us-east-1.amazonaws.com/latest"

This should return a 401 Unauthorized.

Then, if you’re following along, nab the ID token from Cognito (since it has not quite been an hour) and put it in the authorization header:

curl -i -H "Accept: application/json" -H "Authorization: Bearer JWT_ID_TOKEN" "https://API_GATEWAY_ID.execute-api.us-east-1.amazonaws.com/latest"

This should return a 200 with a proper response.

Authentication and Authorization with Amazon Cognito

In general, Amazon Cognito allows you to customize the auth flow via triggers and scopes. The Cognito user pool is standalone and can grant access based on who’s in the pool and what the claims are.

For more complex authorization use cases, it might be best to break users apart into separate pools. This allows for full control over who has more privileges within the system. One nicety of this technique is that Amazon Cognito handles both authentication and authorization, which means your Lambda function does not execute without proper access. This helps in lowering serverless costs.

Note that the API gateway is the one driving authorization because our Lambda function is merely an event instead of a reverse proxy. This technique unlocks the ability to block access without having to execute the Lambda function.

With what we have so far, basic access is granted to the / endpoint. If we wanted higher privileges to call, say, a POST /pizzas to make a pizza, then a separate user pool can handle this requirement.

Final Thoughts on AWS Lambdas with TypeScript

In this series, we built a TypeScript Lambda, improved the dev experience, optimized it, and have now secured it. I hope you’ve found it useful.

The serverless cloud offers many opportunities for developers to iterate quickly and deliver value to happy customers. What is most exciting about this is that it follows the adages from the Unix philosophy that “small is beautiful" and to "make each program do one thing well”.

Happy coding!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

Share this article

RSS
Camilo Reyes

Camilo Reyes

Our guest author Camilo is a Software Engineer from Houston, Texas. He’s passionate about JavaScript and clean code that runs without drama. When not coding, he loves to cook and work on random home projects.

All articles by Camilo Reyes

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps