DEV Community

Cover image for Passwordless Authentication with AWS Cognito: A Step-by-Step Guide
Moin Akhter
Moin Akhter

Posted on

Passwordless Authentication with AWS Cognito: A Step-by-Step Guide

Passwordless authentication is becoming a popular way to authenticate users, offering both convenience and enhanced security. In this tutorial, you’ll learn how to implement passwordless authentication using AWS Cognito, complete with Lambda triggers for creating and verifying one-time passwords (OTPs).

By the end of this tutorial, you’ll understand:

  • What passwordless authentication is
  • How to configure it on AWS Cognito

All code and related resources for this tutorial are available on this GitHub repository.

This tutorial follows the authentication flow illustrated in this diagram.

What is Passwordless Authentication?

Passwordless authentication eliminates the need for users to remember passwords, instead using alternatives like OTPs or links to authenticate. This approach not only reduces the risk of password theft but also enhances user convenience.

Here’s how it works in the context of AWS Cognito:

  • A user enters their email or username on the login page.
  • Cognito triggers Lambda functions to create a challenge (e.g., an OTP) and sends it to the user’s email.
  • The user provides the OTP, which Cognito verifies.
  • Upon successful verification, Cognito returns tokens (access, refresh, and ID tokens) to authenticate the user.
  • Although the concept is simple, setting up this flow requires careful configuration of AWS Cognito and Lambda triggers.

Prerequisites

To follow this tutorial, ensure you have:

An AWS Cognito User Pool: With an associated App Client configured for custom authentication.
An IAM User: With permissions to manage Cognito via the AWS SDK.
Here’s an example of an IAM policy with the required permissions:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "cognito-idp:*",
            "Resource": "RESOURCE_ARN"
        }
    ]
}

Enter fullscreen mode Exit fullscreen mode

Lambda Triggers Overview

Passwordless authentication in AWS Cognito relies on three key Lambda triggers:

DEFINE_AUTH_CHALLENGE: Determines the type of challenge (e.g., OTP) and handles retry limits.
CREATE_AUTH_CHALLENGE: Creates the challenge (e.g., generates and sends an OTP).
VERIFY_AUTH_CHALLENGE: Verifies the user’s response to the challenge.
These triggers work together to implement the passwordless authentication flow.

Implementing Lambda Triggers

1. DEFINE_AUTH_CHALLENGE

This trigger determines the authentication flow logic, such as whether to issue tokens or present the next challenge.

Here’s a sample implementation for allowing up to 3 OTP attempts:

exports.handler = async (event) => {
  const { session } = event.request;
  const lastChallenge = session?.slice(-1)?.[0];

  if (lastChallenge?.challengeResult) {
    // User successfully authenticated
    event.response.issueTokens = true;
    event.response.failAuthentication = false;
  } else if (session?.length >= 3 && !lastChallenge?.challengeResult) {
    // User failed too many attempts
    event.response.issueTokens = false;
    event.response.failAuthentication = true;
  } else {
    // Present next challenge
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'CUSTOM_CHALLENGE';
  }

  return event;
};

Enter fullscreen mode Exit fullscreen mode

2. CREATE_AUTH_CHALLENGE

This trigger generates an OTP and sends it to the user via email.

Here’s a sample implementation using Amazon SES:

const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
const ses = new SESClient();

exports.handler = async (event) => {
  const userEmail = event.userName;
  if (!userEmail) throw new Error('Missing email');

  let otpCode = Math.floor(100000 + Math.random() * 900000); // 6-digit OTP
  await sendEmail(userEmail, otpCode);

  event.response.privateChallengeParameters = { secretLoginCode: otpCode };
  event.response.publicChallengeParameters = { email: userEmail };

  return event;
};

async function sendEmail(emailAddress, otpCode) {
  const command = new SendEmailCommand({
    Destination: { ToAddresses: [emailAddress] },
    Message: {
      Subject: { Data: 'Your One-Time Login Code' },
      Body: { Html: { Data: `<p>Your login code is: <strong>${otpCode}</strong></p>` } }
    },
    Source: process.env.MAIL_FROM
  });

  await ses.send(command);
}
Enter fullscreen mode Exit fullscreen mode

Note: Ensure the Lambda role has ses:SendEmail permissions.

3. VERIFY_AUTH_CHALLENGE

This trigger validates the OTP entered by the user.

Here’s a simple implementation:

exports.handler = async (event) => {
  const expectedOtp = event.request.privateChallengeParameters.secretLoginCode;
  const providedOtp = event.request.challengeAnswer;

  event.response.answerCorrect = providedOtp === expectedOtp;
  return event;
};
Enter fullscreen mode Exit fullscreen mode

Configuring Triggers in Cognito

After deploying the Lambda functions, link them to your Cognito User Pool:

  1. Navigate to the Triggers section of your User Pool.
  2. Assign the Lambda functions to their respective triggers:
    • Define auth challenge → DEFINE_AUTH_CHALLENGE
    • Create auth challenge → CREATE_AUTH_CHALLENGE
    • Verify auth challenge → VERIFY_AUTH_CHALLENGE

Backend API Implementation

Once Cognito is configured, create two backend endpoints to handle authentication.

1. Initiating Authentication

This endpoint starts the custom authentication flow by invoking theCUSTOM_AUTH action:

const { InitiateAuthCommand } = require('@aws-sdk/client-cognito-identity-provider');

const initiateAuth = async (userName) => {
  const command = new InitiateAuthCommand({
    AuthFlow: 'CUSTOM_AUTH',
    ClientId: process.env.AWS_COGNITO_APP_CLIENT,
    AuthParameters: { USERNAME: userName }
  });

  const response = await cognitoClient.send(command);
  return response.Session; // Save this session for verification
};
Enter fullscreen mode Exit fullscreen mode

2. Verifying the OTP

This endpoint verifies the OTP provided by the user:

const { RespondToAuthChallengeCommand } = require('@aws-sdk/client-cognito-identity-provider');

const verifyOtp = async (session, userName, otp) => {
  const command = new RespondToAuthChallengeCommand({
    ChallengeName: 'CUSTOM_CHALLENGE',
    ClientId: process.env.AWS_COGNITO_APP_CLIENT,
    Session: session,
    ChallengeResponses: { USERNAME: userName, ANSWER: otp }
  });

  const response = await cognitoClient.send(command);
  return response.AuthenticationResult; // Contains tokens on success
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

With this setup, you’ve successfully implemented passwordless authentication using AWS Cognito. This method enhances both security and user experience by eliminating passwords. For a complete implementation, visit the GitHub repository.

Feel free to leave a comment if you have questions or suggestions!

Top comments (0)