Securing APIs with AWS Amplify and Cognito
By: Chetan Pawar
Tags: Aws Cognito, Amplify, Rest Api, Fargate, AWS

Overview

AWS Amplify is one of the fastest ways to help front-end web and mobile developers build full stack applications, hosted in AWS.

In the first part of this blog series, Using Amplify for REST APIs and Web hosting we built an API using AWS Amplify to quickly setup and host an API with minimal coding.

In this part we build an authentication system for the API using Amazon Cognito.

Amazon Cognito lets us add user sign-up, sign-in, and access control to our web and mobile apps quickly and easily. It is highly scaleable and supports sign-in with social identity providers, such as Facebook, Google, and Amazon, and enterprise identity providers via SAML 2.0.

Advantages

Secure and scalable user directory

Standards-based authentication

Security for your apps and users

Easy integration with our App

What to Build ?

In this Blog we quickly build an authentication system for a simple REST API using AWS Amplify CLI.

We will build

An authenticated ExpressJS server that returns a random number

Pre-requisites

Amplify CLI — Open terminal and run npm install -g @aws-amplify/clito update to the latest Amplify CLI.

Amplify CLI configured — If you have not configured the Amplify CLI yet, follow this guide in the documentation.

Setup a new Amplify project

Run the following command to create a new Amplify project called “amplify-rest-containerized” or if you already have an existing Amplify project skip to the next section.

mkdir amplify-rest-containerized

cd amplify-rest-containerized

Initialize an Amplify project by running:

amplify init

For a quick implementation, we can just accept the default values in the amplify init workflow console.

Enable container based deployments

Container-based deployments need to be explicitly enabled. Run amplify configure project to review your project configuration:

amplify configure project

Accept the defaults and answer Yes when asked if you want to enable container-based deployments:

...

? Do you want to enable container-based deployments? Yes

Add an authenticated container-based ExpressJS API

For the demo, let’s quickly create a REST API. More detailed explanation can be found here

To create our authenticated container-based REST API, execute:

amplify add api

Choose the following options:

? Please select from one of the below mentioned services: REST

? Which service would you like to use API Gateway + AWS Fargate (Container-based)

? Provide a friendly name for your resource to be used as a label for this category in the project: container622e561f

? What image would you like to use ExpressJS - REST template

? When do you want to build & deploy the Fargate task On every "amplify push" (Fully managed container source)

? Do you want to restrict API access Yes

Successfully added auth resource locally.

Successfully added resource container622e561f locally.

After a successful completion of the CLI workflow, we’ll see these new files added to the project folder structure.

amplify/backend/api/<your-api-name>

├── amplify.state

├── containerb5734e35-cloudformation-template.json

├── parameters.json

└── src

├── Dockerfile

├── DynamoDBActions.js

├── buildspec.yml

├── index.js

├── package-lock.json

└── package.json amplify/backend/auth/<your-api-name>

├── cognitod17bb34c-cloudformation-template.yml

└── parameters.json

In the src/index.js we will find a starter ExpressJS source code to interact with DynamoDB.

Let’s edit that to return a random number.

we will find a starter ExpressJS source code to interact with DynamoDB. Let’s edit that to return a random number. Replace the index.js file with the following code:



const bodyParser = require('body-parser');

const port = process.env.PORT || 3001;const app = express();

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));// Enable CORS for all methods

app.use(function (req, res, next) {

res.header("Access-Control-Allow-Origin", "*")

res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")

next()

});app.get("/", async (req, res, next) => { try {

res.contentType("application/json").send({

"randomNumber": Math.floor(Math.random() * 101)

})

} catch (err) {

next(err);

}

});app.listen(port, () => {

console.log('Example app listening at

}); const express = require("express");const bodyParser = require('body-parser');const port = process.env.PORT || 3001;const app = express();app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: true }));// Enable CORS for all methodsapp.use(function (req, res, next) {res.header("Access-Control-Allow-Origin", "*")res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")next()});app.get("/", async (req, res, next) => { try {res.contentType("application/json").send({"randomNumber": Math.floor(Math.random() * 101)})} catch (err) {next(err);});app.listen(port, () => {console.log('Example app listening at http://localhost:' + port);});

Deploy

Now let’s deploy our API:

amplify push

✔ Successfully pulled backend environment dev from the cloud. Current Environment: dev | Category | Resource name | Operation | Provider plugin |

| -------- | ----------------- | --------- | ----------------- |

| Auth | cognitod17bb34c | Create | awscloudformation |

| Api | container622e561f | Create | awscloudformation |

? Are you sure you want to continue? Yes

As we see amplify also deploys authentication related resources.

What’s happening under the hood ?

We are deploying an API just like the first blog

Adding authentication related resources like AWS Cognito User Pool, Identity Pool, User Pool Client, Client Role, Authorizer Lambda function

Amplify uses a Nested CloudFormation Stack to deploy each component from the App. In this example, it would deploy a network stack, a cognito resources stack and a containerized API stack.

Test our containerized and authenticated ExpressJS API

This is where we come to the tricky part.

There are several ways in which we can interact with AWS Cognito APIs to get the necessary authentication token for getting access to the API.

AWS SDK for JavaScrip t to include components in your code to obtain token using pre-built UI components in React.

to include components in your code to obtain token using pre-built UI components in React. AWS CLI commands to perform sign-up for a user, confirm the user registration and request for generating a token for authentication.

For quick testing, we would use the AWS CLI generated token for authenticating our request to the API.

Pre-requisites to generate Token

You would need the following information before you start the process

The AppClient-ID for the AWS Cognito User Pool — This can be obtained from AWS Console for AWS Cognito or from the CloudFormation Stack Resources Details Tab

for the AWS Cognito User Pool — This can be obtained from AWS Console for AWS Cognito or from the CloudFormation Stack Resources Details Tab The UserPool-ID for the AWS Cognito User Pool — This can be obtained from AWS Console for AWS Cognito or from the CloudFormation Stack Resources Details Tab

How to generate the token ?

Create a Sign-up request for the required User

aws cognito-idp sign-up --region <region> --client-id <client_id> --username xyz.abc --password xxxx --user-

attributes Name="email",Value="xyz.abc@efg.com" Response :

{

"UserConfirmed": false,

"CodeDeliveryDetails": {

"Destination": "xxx@xxx.com",

"DeliveryMedium": "EMAIL",

"AttributeName": "email"

},

"UserSub": "0bf218e6-65ab-4160-8ae9-48bb4e9f22cd"

}

Confirm the registration of the User

aws cognito-idp admin-confirm-sign-up --region <region> --user-pool-id <user_pool_id> --username xyz.abc

The password set for the new user in Step 1 is temporary and it needs to be changed before generating the authentication request

aws cognito-idp initiate-auth \

--client-id <client_id> \

--auth-flow USER_PASSWORD_AUTH \

--auth-parameters "USERNAME=xyz.abc,PASSWORD=xxxxxxxx" Response:



{

"ChallengeName": ' NEW_PASSWORD_REQUIRED ',

'ClientId' => ' <client_id> ',

'Session' => <session_token>,

}

Use the session_token from the above response to set the new password

aws cognito-idp respond-to-auth-challenge --client-id <client_id> --challenge-name NEW_PASSWORD_REQUIRED --challenge-responses USERNAME=xyz.abc,NEW_PASSWORD="xxxxxxxx" --session " <session_token> "

The above request responds with the Tokens generated after successful authentication with AWS Cognito

{

"ChallengeParameters": {},

"AuthenticationResult": {

"AccessToken": "xxxxxxxxxxxxxxxxxxx",

"ExpiresIn": 3600,

"TokenType": "Bearer",

"RefreshToken": "xxxxxxxxxxxxxxxxxxx",

"IdToken": "xxxxxxxxxxxxxxxxxxx"

}

}

Finally we can use the AccessToken to test our API

Testing the API

The best way to demonstrate the authenticated API is from the Postman App using Authentication.

Setup authentication parameters in Postman App — Enter the AccessToken in the Token section.

Execute the request to the API

The API endpoint is printed at the end of the “amplify push” command or when you execute “amplify status”.

Note: This is a simple use case just to showcase the workflow.

We used the AWS CLI to quickly demonstrate the authentication flow. However, actual authentication for your app should be done seamlessly using the appropriate AWS SDK for your code.

Review Amplify documentation if you’re interested in any other scenario.

What did we learn ?

This blog post gives a quick way to deploy an authenticated API using Amplify CLI.

Quickly setup and authenticated API using AWS Amplify

How to authenticate the requests to the authenticated API using AWS CLI

There is so much more to explore.

Keep building…