December 7, 2022

Blog @ Munaf Sheikh

Latest news from tech-feeds around the world.

AWS Cognito Authentication With Serverless and NodeJS

Great post from our friends at Source link

In this post, we are going to see how we can create a REST API application for authentication using AWS Cognito, AWS Serverless, and NodeJS. We are going to use Lambda functions, API Gateway, and Serverless framework to achieve this.

Let’s start by setting up the project.

Project Setup

Our project structure will look like this.


As we can see that we are storing all our lambda functions file in a folder named user and all our utility functions in a separate folder called functions, other than that there is a serverless.yml file which is a core file for any serverless based project.

Serverless.yml File

Let’s start coding our serverless.yml file where we will be defining all our lambda functions which will be holding our logic for Sign up, Sign in, etc, we will also define our AWS Cognito user pool and user pool client with different settings and permissions.

Let’s break this file into different parts so we can understand each part separately.

Defining AWS IAM Permissions and Settings

We will start by defining things like environment variables, serverless project configuration, settings, and AWS IAM permissions.

service: serverless-cognito-auth

provider:
  name: aws
  runtime: nodejs14.x
  environment:
    user_pool_id: { Ref: UserPool }
    client_id: { Ref: UserClient }
  iamRoleStatements:
    - Effect: Allow
      Action:
        - cognito-idp:AdminInitiateAuth
        - cognito-idp:AdminCreateUser
        - cognito-idp:AdminSetUserPassword
      Resource: "*"

Under provider block we are defining multiple configurations and settings. Let’s discuss it in brief.

  • environment – In this block, we define all our environment variables which we want to use in our project, like in our lambda functions, etc. We are setting the user pool id and client id of our AWS Cognito user pool and client. We are referencing the resources which we are going to define later on in this file so don’t worry about that, just understand that these references are going to give us the id for the created user pool and client.
  • iamRoleStatements – In this block, we define all the AWS IAM permissions which we want to give to our resources, in our case these permissions are required by our lambda functions which are going to use AWS Cognito API.

To read more about AWS IAM, check out the official documentation.

Defining Lambda Functions

Next, we will define our lambda functions, we are going to need three of them, for user registration, user login, and the last one to test a private route.

functions:
  loginUser:
    handler: user/login.handler
    events:
      - http:
          path: user/login
          method: post
          cors: true

  signupUser:
    handler: user/signup.handler
    events:
      - http:
          path: user/signup
          method: post
          cors: true

  privateAPI:
    handler: user/private.handler
    events:
      - http:
          path: user/private
          method: post
          cors: true
          authorizer:
            name: PrivateAuthorizer
            type: COGNITO_USER_POOLS
            arn:
              Fn::GetAtt:
                - UserPool
                - Arn
            claims:
              - email

  • events – In this block, we define the event on which our lambda function will get invoked, so in our case, we are adding an HTTP event here, which will be our AWS API Gateway call.
  • authorizer – Here we define our authorizer which will get called before our main lambda function gets invoked, so here we are using AWS Cognito authorizer for our API Gateway which checks on each request if the valid access token is being passed with it, then only it allows our main lambda function to be invoked.

We need to pass ARN of our AWS Cognito user pool, so we are referencing that resource and getting the ARN from it by using :GetAtt function. We are also using claims block which is used to have the specific fields available from the decoded access token object in our main lambda function in the event object.

Defining Resources

In the last, we are going to define all the resources which we need in our serverless.yml file.

resources:
  Resources:
    UserPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: serverless-auth-pool
        Schema:
          - Name: email
            Required: true
            Mutable: true
        Policies:
          PasswordPolicy:
            MinimumLength: 6
        AutoVerifiedAttributes: ["email"]

    UserClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
        ClientName: user-pool-ui
        GenerateSecret: false
        UserPoolId: { Ref: UserPool }
        AccessTokenValidity: 5
        IdTokenValidity: 5
        ExplicitAuthFlows:
          - "ADMIN_NO_SRP_AUTH"

Here we are creating our AWS Cognito user pool and client, let’s go through some of the options here, if you want to see all the options which you can use, check this official documentation and this one as well for user pool client.

  • Schema – Here we define the schema of the user data which will be created in our user pool, we can define different attributes like email, age, gender, etc.
  • Policies – In this block, we define our password validation policy so basically all the settings of how the password should be before it can get saved in our user pool.
  • AutoVerifiedAttributes – Here we can set the fields which we want to be automatically verified like email and phone number, generally when a new user gets created in the AWS Cognito user pool that user has to go through a verification process to verify their email or phone number, but setting that field here is going to skip that verification process for the created user.
  • AccessTokenValidity – This defines the number of hours the access token will be valid.
  • ExplicitAuthFlows – This defines all the authentication flows which will be allowed by the user pool client, we are going to use ADMIN_NO_SRP_AUTH which can be used to authorize users with username and password, that’s why we are passing it here as the value.

I encourage you to also check out the official documentation of AWS Cognito and also check out AWS Cognito Pricing.

Coding the Lambda Functions

It’s now time to start coding our REST API logic by creating lambda functions for user registration, user login, and our private route to test everything out.

User Registration

First, we are going to create a new file inside the user folder and name it signup.js, this file will hold all the logic related to user registration, let’s see how the code will look like in this file by breaking it into parts.

Imports

const AWS = require('aws-sdk')
const { sendResponse, validateInput } = require("../functions");

const cognito = new AWS.CognitoIdentityServiceProvider()

We are going to use aws-sdk NPM to interact with AWS Cognito API and we are also importing two utility functions (check out the code) sendResponse for sending the response of the HTTP request and validateInput is for validating the request body data. We are also getting the instance of Cognito identity provider to interact with the user pool API.

Validating Request Body Data

const isValid = validateInput(event.body)
if (!isValid)
return sendResponse(400, { message: 'Invalid input' })

Here we are validating the request body data and checking if data is valid or not, if it is not valid we are returning the response and sending an appropriate message.

Creating a User in AWS Cognito User Pool

const {
 email,
 password
 } = JSON.parse(event.body)
const {
 user_pool_id
 } = process.env

const params = {
  UserPoolId: user_pool_id,
  Username: email,
  UserAttributes: [{
      Name: 'email',
      Value: email
    },
    {
      Name: 'email_verified',
      Value: 'true'
    }
  ],
  MessageAction: 'SUPPRESS'
}
const response = await cognito.adminCreateUser(params).promise();

Here we are getting the email and password from the request body and also the user pool id from the environment variables object. After that, we are creating a parameter object for adminCreateUser API, MessageAction is set as ‘SUPPRESS’ because we don’t want to send the default email sent by AWS Cognito when a new user gets created in the user pool.

Setting the Password for the Created User

if (response.User) {
  const paramsForSetPass = {
    Password: password,
    UserPoolId: user_pool_id,
    Username: email,
    Permanent: true
  };
  await cognito.adminSetUserPassword(paramsForSetPass).promise()
}
return sendResponse(200, {
  message: 'User registration successful'
})

When our user gets created in the user pool, we need to set the password for that user because we don’t want users to create a password when they log in as they are already sending their password in the HTTP request, this will also change the user status as CONFIRMED in the Cognito user pool, we also need to pass Permanent as true because otherwise a temporary password will be generated for the user.

User Login

Now we will start with the user login by creating a file inside the user folder named login.js. This login API will start the authentication process and send the identity token to the user which they can use to access the authorized routes.

login.js will look very similar tosignup.js; the only difference will be the parameters and the API call.

Starting the Authentication Process

const {
  email,
  password
} = JSON.parse(event.body)
const {
  user_pool_id,
  client_id
} = process.env

const params = {
  AuthFlow: "ADMIN_NO_SRP_AUTH",
  UserPoolId: user_pool_id,
  ClientId: client_id,
  AuthParameters: {
    USERNAME: email,
    PASSWORD: password
  }
}
const response = await cognito.adminInitiateAuth(params).promise();
return sendResponse(200, {
  message: 'Success',
  token: response.AuthenticationResult.IdToken
})

The main thing to understand in this code is that we are using AuthFlow as ADMIN_NO_SRP_AUTH which is used for authenticating the user based on username and password, after that, we are just calling the adminInitiateAuth API and sending the identity token to the user.

Private Route

We will add one more lambda function which will act as a private route, to access this API endpoint we will need to send a valid identity token in the request header with the key ‘Authorization’, start by creating a new file inside the user folder and name it as private.js.

module.exports.handler = async (event) => {
  return sendResponse(200, {
    message: `Email ${event.requestContext.authorizer.claims.email} has been authorized`
  })
}

Here we are just getting the email from the request and sending a simple response, this lambda function will only get invoked if the request passes the authorizer layer added in the API Gateway configuration.

To check all the API offered by Nodejs SDK check this out.

Conclusion

Now you have the REST API for authentication using AWS Cognito, AWS Serverless, and Nodejs. Make sure to check out the Github code given at the end of this post, there are many things that can be added or improved in the current code like data validation can be increased, forget password can be added, etc.

Get the Code

Source code on Github.

#AWS #Cognito #Authentication #Serverless #NodeJS