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"
}
]
}
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;
};
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);
}
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;
};
Configuring Triggers in Cognito
After deploying the Lambda functions, link them to your Cognito User Pool:
- Navigate to the Triggers section of your User Pool.
- 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
};
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
};
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)