Front-End Web & Mobile
Implementing passwordless email authentication with Amazon Cognito
June 27, 2024: This blog post covers Amplify Gen 1. For new Amplify apps, we recommend using Amplify Gen 2. You can learn more about Gen 2 in our launch blog post.
Having to remember passwords can be a pain, especially for passwords that you don’t use often. Like most people, you too are probably familiar with having to click the “forgot password” link or button on websites and apps.
Many people are tempted to use bad practices such as using short passwords, using easily guessable passwords, reusing the same password on many sites and apps, and so on. Even though there are solutions for this (for example, password managers), in practice, password-based security isn’t that safe, and certainly isn’t that user friendly.
There are alternatives to logging in with passwords—for example, using a fingerprint scan or facial recognition. But it’s not always feasible to use such methods.
Amazon Cognito provides you another alternative. What if you didn’t have to enter a password when you log in, but the website or app just sends you a temporary one-time login code, for example, through email, SMS, or a push notification? You retrieve the code, enter it, and you’re in. It’s like a “forgot password” process, but simpler and shorter. Also, it doesn’t carry the notion that you forgot your password.
With Amazon Cognito user pools, you can create custom authentication flows. In this blog post, we demonstrate how this is done by going through a sample implementation of a passwordless authentication flow that sends a one-time login code to the user’s email address.
Overview of the solution
The passwordless email authentication solution uses an Amazon Cognito user pool and a couple of Lambda functions. You use these together to implement the custom authentication flow. You use Amazon Simple Email Service (Amazon SES) for sending the emails with the one-time login codes. Also, the sign-in process is supported by custom UI pages (HTML and JavaScript).
The diagram and the following steps describe the process for the solution.
- The user enters their email address on the custom sign-in page, which sends it to the Amazon Cognito user pool.
- The user pool calls the “Define Auth Challenge” Lambda function. This Lambda function determines which custom challenge needs to be created.
- The user pool calls the “Create Auth Challenge” Lambda function. This Lambda function generates a secret login code and mails it to the user by using Amazon SES.
- The user retrieves the secret login code from their mailbox and enters it on the custom sign-in page, which sends it to the user pool.
- The user pool calls the “Verify Auth Challenge Response” Lambda function. This Lambda function verifies the code that the user entered.
- The user pool calls the “Define Auth Challenge” Lambda function. This Lambda function verifies that the challenge has been successfully answered and that no further challenge is needed. It includes “issueTokens: true” in its response to the user pool. The user pool now considers the user to be authenticated, and sends the user valid JSON Web Tokens (JWTs) (in the response to 4).
Serverless application
The sample implementation is packaged as a serverless application. Deploy it from the AWS Serverless Application Repository, and see how it works.
Relevant parts of the user pool’s configuration are:
- It requires email addresses as user names.
- It has an app client that’s configured to “Only allow custom authentication”. This is because when a user signs up, Amazon Cognito requires a password. We’ll supply a random string for this. We don’t want users to be able to actually log in with this password later.
- The following Lambda functions (Node.js 8.10) that implement the custom authentication flow are configured:
- Define Auth Challenge – This Lambda function tracks the custom authentication flow, which is comparable to a decider function in a state machine. It determines which challenges should be presented to the user in which order. At the end, it reports back to the user pool if the user succeeded or failed authentication. The Lambda function is invoked at the start of the custom authentication flow and also after each completion of the “Verify Auth Challenge Response” trigger.
- Create Auth Challenge – This Lambda function is invoked, based on the instruction of the “Define Auth Challenge” trigger, to create a unique challenge for the user. We’ll use it to generate a one-time login code and mail it to the user.
- Verify Auth Challenge Response – This Lambda function is invoked by the user pool when the user provides the answer to the challenge. Its only job is to determine if that answer is correct.
- Users can sign up themselves and are auto-confirmed using the pre sign-up user pool trigger. Because email addresses are integral to being able to log in, we don’t need users to separately confirm their email address.
Create Auth Challenge trigger
Read through the code, and you’ll see that it generates a secret login code and mails this to the user. A user has three chances to enter the right code, before needing to be sent a new login code.
Verify Auth Challenge trigger
All this function has to do is validate that the user’s answer matches the secretLoginCode.
Define Auth Challenge trigger
This is the decider function that manages the authentication flow. In the session array that’s provided to this Lambda function (event.request.session), the entire state of the authentication flow is present.
If it’s empty, the custom authentication flow just started. If it has items, the custom authentication flow is underway: a challenge was presented to the user, the user provided an answer, and it was verified to be right or wrong. In either case, the decider function has to decide what to do next:
import { CognitoUserPoolTriggerHandler } from 'aws-lambda';
export const handler: CognitoUserPoolTriggerHandler = async event => {
if (event.request.session &&
event.request.session.find(attempt => attempt.challengeName !== 'CUSTOM_CHALLENGE')) {
// We only accept custom challenges; fail auth
event.response.issueTokens = false;
event.response.failAuthentication = true;
} else if (event.request.session &&
event.request.session.length >= 3 &&
event.request.session.slice(-1)[0].challengeResult === false) {
// The user provided a wrong answer 3 times; fail auth
event.response.issueTokens = false;
event.response.failAuthentication = true;
} else if (event.request.session &&
event.request.session.length &&
event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE' && // Doubly stitched, holds better
event.request.session.slice(-1)[0].challengeResult === true) {
// The user provided the right answer; succeed auth
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
// The user did not provide a correct answer yet; present challenge
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
return event;
};
Pre sign-up trigger
This function auto-confirms users and their email addresses during sign-up:
Implementing the custom sign-in page
To coordinate with the user pool’s custom authentication flow, a custom sign-in page is needed. You can use the AWS Amplify Framework to integrate your custom sign-in page with Amazon Cognito.
You can implement the custom sign-in page with your favorite framework (React, Angular, Vue, plain HTML/JavaScript, etc.). The following are some JavaScript (TypeScript) examples in the sample solution.
The following shows initializing the AWS Amplify Framework in JavaScript:
Signing up
For users to be able to sign themselves up, we have to “generate” a password for them, because a password is required by Amazon Cognito when users sign up.
Signing in
Initiate the authentication and start the custom flow:
Answering the custom challenge
The user should check their mail and retrieve the secret login code. When the user has entered their secret login code, invoke AWS Amplify to send the secret login code to the user pool as the answer to the custom challenge. Then, one of three things can happen:
- The user has entered the right code. The user pool responds to AWS Amplify with JWTs, which AWS Amplify stores in the user’s browser (by default in local storage).
- The user didn’t enter the right code, but it wasn’t the third time yet, so the user has another chance to enter the right code.
- The user didn’t enter the right code, and it was the third time already, so the authentication failed. The user must go back to the sign-in page and start a new custom authentication flow.
Timeout of 3 minutes
Note that after being given the custom challenge, the user has 3 minutes to actually retrieve the secret login code from their email and provide it as the answer. After that, the custom authentication flow times out, and the user has to acquire a new secret login code by starting a new custom authentication flow. This 3-minute timeout is enforced server side by Amazon Cognito. It’s the same as the timeout for code entry with multi-factor authentication (MFA).
Summary
We’ve implemented passwordless authentication with secret login codes sent by email, by using Amazon Cognito custom authentication flows. Depending on the security requirements of your website or app, this solution might work for you as a suitable balance between security and user friendliness. Because the solution is built with Lambda functions, you can extend and adapt it to your needs. Read more about custom authentication in the developer guide and have fun coding!
As always, we’d love to hear from you. You can comment below or reach out to us on the Amazon Cognito Forum.
Resources
- Check out the code on GitHub to see how the sample solution is built. You can deploy and run the code yourself.
- You can deploy the Amazon Cognito resources from the sample solution directly from the AWS Serverless Application Repository.
Otto Kruse is a professional services consultant based in the Netherlands and focusses on app development practices.