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:
- Allowing the username and password auth flow.
- 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:
- Go to AWS, and click API Gateway.
- Open up
pizza-api
and inspect the Authorizers. There should be apizza-api-cognito
authorizer. - Check the API endpoints under Resources. The main
/
endpoint should haveCOGNITO_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.
Top comments (0)