In this comprehensive guide, we will demonstrate how to integrate AWS Cognito with a Serverless application to handle user registration, authentication, and data management. This powerful combination allows you to build secure and scalable serverless APIs with user authentication and data storage features. We'll break down the process step by step and provide code snippets for each task.
Overview:
Our objective is to create a Serverless API using Node.js Lambda functions that enable user registration and list countries from DynamoDB for authenticated users. We'll leverage the Serverless Framework for this purpose.
Acceptance Criteria:
we aim to achieve the following :
1- Register users through the Serverless API and verify their email addresses.
2- Enable users to list countries using a REST client by providing a token obtained during registration.
4- Store user data securely in DynamoDB.
Architecture:
Step-by-Step Implementation:
Step 1: Install Serverless Framework
The Serverless Framework simplifies the deployment and management of serverless applications. To get started, you can install it globally using npm (Node Package Manager).
npm install serverless -g
This command installs the Serverless Framework globally on your machine, allowing you to create and manage serverless projects with ease.
Step 2: Create a Serverless Project
The Serverless Framework provides a convenient way to create a new serverless project. You can use predefined templates to scaffold your project.
serverless create --template aws-nodejs --path my-service
This command creates a new serverless project in a directory called my-service. Inside this directory, you'll find essential project files such as serverless.yml and handler.js.
Step 3: Create a DynamoDB Table for Countries
In your serverless.yml file shown below, you need to define the DynamoDB table that will store country data. DynamoDB is a fully managed NoSQL database service provided by AWS.
serverless.yml
# Service name has to be unique for your account.
service: my-service
# framework version range supported by this service.
frameworkVersion: '2'
# Configuration of the cloud provider. As we are using AWS so we defined AWS corresponding configuration.
provider:
name: aws
runtime: nodejs14.x
#lambdaHashingVersion: 20201221
stage: dev
region: us-east-2
# Create an ENV variable to be able to use it in my JS code. *** Check line 4 in get-country-by-name JS file ***
environment:
countriestableName: ${self:custom.countriestableName}
userstableName: ${self:custom.userstableName}
# To Give a permission to each lambda function to access DynamoDB table
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:*
# - dynamodb:Query
# - dynamodb:Scan
# - dynamodb:GetItem
# - dynamodb:PutItem
Resource: "*"
custom:
countriestableName: countriees
userstableName: useres
userPoolCallback: http://localhost
# As shown below, when the HTTP POST or GET request is made, the handler should be invoked.
functions:
#(1) Lambda function to initially fill DynamoDB
FillDynamoDB:
handler: lambdas/common/countries_loadData.fill
description: fill DynamoDB table with set of countries.
events:
- http:
path: fill-dynamoDB
method: POST
cors: true
#(2) Lambda function to list all the countries
GetAllCountries:
handler: lambdas/lambda-endpoints/list-countries.list
description: get all the countries information.
events:
- http:
path: list-countries
method: GET
cors: true
integration: lambda
authorizer:
#name: ${self:resources.Resources.ApiGatewayAuthorizer.Properties.Name}
name: CognitoUserPoolAuthorizer
# The type of authorizer. COGNITO_USER_POOLS: An authorizer that uses Amazon Cognito user pools.
type: COGNITO_USER_POOLS
scopes: email
# The source of the identity in an incoming request.
identitySource: method.request.header.Authorization
# The ID of the RestApi resource that API Gateway creates the authorizer in.
RestApiId: ApiGatewayRestApi
arn:
Fn::GetAtt:
- UserPool
- Arn
#(3) Lambda function to add the user data in users dynamoDB table
RegisterNewUser:
handler: lambdas/lambda-endpoints/addusertoDB.putNewUser
description: get all the countries information.
events:
- http:
path: add-new-user
method: POST
cors: true
#Resources are AWS infrastructure components which your Functions use.
#The Serverless Framework deploys an AWS components your Functions depend upon.
resources:
${file(./CF.yaml)}
CF.yaml
Resources:
countriesDynamoDbTable:
Type: 'AWS::DynamoDB::Table'
#DeletionPolicy: Retain
Properties:
TableName: ${self:custom.countriestableName}
AttributeDefinitions:
-
AttributeName: "NAME"
AttributeType: "S"
KeySchema:
-
AttributeName: "NAME"
KeyType: "HASH"
BillingMode: PAY_PER_REQUEST
usersDynamoDbTable:
Type: 'AWS::DynamoDB::Table'
#DeletionPolicy: Retain
Properties:
TableName: ${self:custom.userstableName}
AttributeDefinitions:
-
AttributeName: "NAME"
AttributeType: "S"
KeySchema:
-
AttributeName: "NAME"
KeyType: "HASH"
BillingMode: PAY_PER_REQUEST
# Custom resource to invoke lambda function to fill the countries DynamoDB table
TriggerFillDynamoDBFunction:
Type: AWS::CloudFormation::CustomResource
#DependsOn: !Ref FillDynamoDB
Properties:
#ServiceToken: arn:aws:lambda:us-east-2:944163165741:function:my-service-dev-FillDynamoDB
ServiceToken: !GetAtt 'FillDynamoDBLambdaFunction.Arn'
# User Pool Resources
# Cognito User Pool Resource
UserPool:
Type: AWS::Cognito::UserPool
Properties:
AdminCreateUserConfig:
UserPoolName: ahmedsalem-UserPool
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
AutoVerifiedAttributes:
- email
UsernameConfiguration:
CaseSensitive: false
AccountRecoverySetting:
RecoveryMechanisms:
- Priority: 1
Name: "verified_email"
LambdaConfig:
PostConfirmation: !GetAtt RegisterNewUserLambdaFunction.Arn
UserPoolToRegisterNewUserLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt RegisterNewUserLambdaFunction.Arn
Principal: cognito-idp.amazonaws.com
Action: lambda:InvokeFunction
SourceArn: !GetAtt UserPool.Arn
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: ahmedsalem-UserPoolClient
UserPoolId: !Ref UserPool
GenerateSecret: false
AllowedOAuthFlows:
- implicit
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthScopes:
- phone
- email
- openid
- profile
- aws.cognito.signin.user.admin
CallbackURLs:
- ${self:custom.userPoolCallback}
SupportedIdentityProviders:
- COGNITO
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: ahmedsalem-app
UserPoolId: !Ref UserPool
Outputs:
TokenURL:
Description: Url for users signing in/up
Value: !Sub "https://${UserPoolDomain}.auth.us-east-2.amazoncognito.com/oauth2/authorize?response_type=token&client_id=${UserPoolClient}&redirect_uri=${self:custom.userPoolCallback}"
Step 4: Create Lambda function to initially fill countries DynamoDB table in serverless.yaml file and its backend nodejs code.
API_Responses.js
const Responses = {
_200(data = {}) {
return {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Origin': '*',
},
statusCode: 200,
body: JSON.stringify(data),
};
},
_400(data = {}) {
return {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Origin': '*',
},
statusCode: 400,
body: JSON.stringify(data),
};
},
};
module.exports = Responses;
countries.json
[
{
"name": "Albania",
"code": "AL"
},
{
"name": "Australia",
"code": "AU"
},
{
"name": "Belgium",
"code": "BE"
},
{
"name": "Brazil",
"code": "BR"
},
{
"name": "China",
"code": "CN"
},
{
"name": "Greece",
"code": "GR"
}
]
Dynamo.js
// To access AWS resources
const AWS = require('aws-sdk');
// when you're working with dynamoDB you have to use the document client to acces the files in DynamoDB
const documentClient = new AWS.DynamoDB.DocumentClient();
// Create an obj named Dynamo that has a get method that takes an NAME and tablename
// this a Async process looking into DynamoDB, So we need to await this
const Dynamo = {
async get(NAME, TableName) {
const params = {
TableName,
Key: {
NAME,
},
};
const data = await documentClient.get(params).promise();
if (!data || !data.Item) {
throw Error(`There was an error fetching the data for NAME of ${NAME} from ${TableName}`);
}
console.log(data);
return data.Item;
},
async write(data, TableName) {
if (!data.NAME) {
throw Error('no NAME on the data');
}
const params = {
TableName,
Item: data,
};
const res = await documentClient.put(params).promise();
if (!res) {
throw Error(`There was an error inserting NAME of ${data.NAME} in table ${TableName}`);
}
return data;
},
async list(TableName) {
if (!data) {
throw Error('error in listing all countries');
}
const params = {
TableName
};
const res = await documentClient.scan(params).promise();
if (!res) {
throw Error(`error in listing all countires ${TableName}`);
}
return data;
},
};
module.exports = Dynamo;
countries_loadData.js
var AWS = require("aws-sdk");
var fs = require('fs');
const Responses = require('../common/API_Responses');
const Dynamo = require('../common/Dynamo');
const countriestableName = process.env.countriestableName;
var response = require('cfn-response');
var documentClient = new AWS.DynamoDB.DocumentClient();
console.log("Importing countries into DynamoDB. Please wait...");
var allcountries = JSON.parse(fs.readFileSync('lambdas/common/countries.json', 'utf8'));
console.log(allcountries);
exports.fill = (event, context) => {
if (event.RequestType != "Create") {
return response.send(event, context, response.SUCCESS, {})
}
allcountries.forEach( function(country) {
var params = {
TableName: countriestableName,
Item: {
"NAME": country.name,
"Code": country.code
}
};
documentClient.put(params, function(err, data) {
if (err) {
console.error("Unable to add country", country.name, ". Error JSON:", JSON.stringify(err, null, 2));
return response.send(event, context, response.FAILED, {})
} else {
console.log("PutItem succeeded:", country.name);
return response.send(event, context, response.SUCCESS, {})
}
});
});
};
Step 5: Create a Custom Resource to fill the DynamoDB Table
To automate the population of the DynamoDB table during deployment, you can define a custom resource in your serverless.yml file above. This custom resource triggers the Lambda function responsible for filling the countries' DynamoDB table.
Step 6: Create Lambda Function for Listing Countries
Create a Lambda function to list countries from the DynamoDB table. This function should be defined in Node.js and include code for querying DynamoDB.
// const Responses = require('../common/API_Responses');
const AWS = require('aws-sdk');
const Dynamo = require('../common/Dynamo');
const documentClient = new AWS.DynamoDB.DocumentClient();
const countriestableName = process.env.countriestableName;
const params = {
TableName : countriestableName
}
async function listItems(){
try {
const data = await documentClient.scan(params).promise()
return data
} catch (err) {
return err
}
}
exports.list = async (event, context) => {
try {
const data = await listItems()
return { body: JSON.stringify(data) }
} catch (err) {
return { error: err }
}
}
Step 7: Access the API
To access your Serverless API, you can use the provided URLs. Utilize a REST client to test the API, specifically the /list-countries endpoint.
Step 8: Create a DynamoDB Table for Users
Similar to step 3, you need to define another DynamoDB table to store user data. This table will hold user information after registration.
Step 9: Create a Cognito User Pool
Amazon Cognito is an identity management service that simplifies user authentication and authorization. Here, you create a Cognito user pool, which is essentially a user directory where your users can sign up and sign in.
a) Create a User Pool
b) Create a Lambda Permission to Register New Users
Create a Lambda permission to allow Cognito to invoke your Lambda function for registering new users. This step establishes the connection between Cognito and your Lambda function.
c) Create a User Pool Client
A user pool client represents a web or mobile application that interacts with your Cognito User Pool. Define the client properties to configure how users interact with your app.
d) Create a User Pool Domain
A user pool domain provides a custom domain name for your Cognito user pool's hosted UI. It's used for user sign-up and sign-in.
Step 10: Implement Lambda Function for User Registration
Create a Lambda function that handles user registration. This function should be triggered when a user signs up through the Cognito user pool. Ensure that you have installed any required dependencies for your Lambda function.
const {
DynamoDBClient,
PutItemCommand,
} = require("@aws-sdk/client-dynamodb");
console.log("hello");
const dynamoDbClient = new DynamoDBClient();
const USER_TABLE = process.env.userstableName;
module.exports.putNewUser = async (event, context, callback) => {
console.log(USER_TABLE);
console.log(event.request.userAttributes.email)
console.log(event.userName)
await dynamoDbClient.send(new PutItemCommand({
TableName: USER_TABLE,
Item: {
NAME: {S: event.userName},
Email: {S: event.request.userAttributes.email}
}
}))
callback(null, event)
};
Step 11: Test User Registration
To Confirm that lambda function will to be fired when new user sign up using AWS Cognito and its data will be saved in users table.
(a) Go to the created User Pool and select "App Client Settings"
(b) Click on "Launch Hosted UI"
(c) Click on sign up, add new user, and paste the confirmation code that will be sent to you through the mail
(d) Check the users DynamoDB table, it should have the new registered user data.
Step 12: Create an API Authorizer
API authorizers are used to control access to your API endpoints. In this step, you create an API authorizer that checks the access_token header in the request to ensure that only authorized users can access your API.
Step 13: Test API Authorization
To confirm that the API endpoint is restricted to authorized users, follow these steps:
1- Go to your Cognito User Pool settings.
2- Select "App Client Settings."
3- Launch the hosted UI.
4- Sign in with a user account.
5- After signing in, you'll be directed to a rollback URL;
6- copy the URL to obtain the access_token.
7- Open a REST client and paste the API endpoint URL.
8- Set the action to "GET."
9- If you attempt to send the request without the access_token, you should receive an "unauthorized request" response.
10- Add an "Authorization" header with the access_token, and you should receive the expected response from the API endpoint.
Step 14: Deploy Your Serverless Application
Deploying your Serverless application is straightforward using the Serverless Framework. The sls deploy command packages and deploys your entire application stack to AWS.
sls deploy
This command uploads your Lambda functions, API Gateway, and other resources to AWS.
Step 15: Remove Resources (Optional)
If you ever need to remove all the functions, events, and resources created by your Serverless application from your AWS account, you can use the sls remove command.
sls remove
Conclusion:
In this article, we've covered the complete process of integrating AWS Cognito with a Serverless application for user registration, authentication, and data management. Leveraging the power of Serverless and AWS Cognito, you can build secure and scalable serverless APIs with user authentication.
Top comments (0)