Implementing passwordless login with Amazon Cognito
Intro I'm going to skip the introduction and description of what Cognito is and why it's useful—I’ll assume you have a reason for reading this. I’ll use Node.js with AWS SDK V3 for the code snippets, but the language doesn’t matter—you should easily find equivalents in other SDKs. Please note that this is not a ready-to-use solution you can copy-paste into your app. Instead, think of it as a set of building blocks you can use to design an authentication flow that meets your specific requirements. This post is based on the latest updates in Cognito (as of around November 2024), which no longer require implementing custom challenges. So, let’s get started! Setup user pool The first thing you need is a user pool. Setting up passwordless login is quite straightforward. In the pool's overview, you should see a tile similar to the one shown below: Next, navigate to the configuration for choice-based sign-in: Enable one-time password (OTP) by selecting the appropriate checkbox: For basic usage (perhaps for evaluation purposes), simply select Send email with Cognito and click Save changes: For more advanced use cases, setting up Amazon SES is required. However, this topic will not be covered in this post. Setup sdk We need the aws-sdk library to interact with AWS services: npm install @aws-sdk/client-cognito-identity-provider --save Once the SDK is downloaded, set up the client. The snippet below includes all the imports used throughout this post: import { CognitoIdentityProviderClient, InitiateAuthCommand, RespondToAuthChallengeCommand, AdminGetUserCommand, AdminCreateUserCommand } from "@aws-sdk/client-cognito-identity-provider"; const client = new CognitoIdentityProviderClient({region: "eu-north-1"}); Check existence of user Cognito sends emails only to users that exist in the pool. Therefore, we must first check if this is the case. There is no need to verify the contents of the response, as the call will throw an exception for a non-existing user: async function isExistingUser(username) { try { const getUserCommand = new AdminGetUserCommand({Username: username, UserPoolId: USER_POOL_ID}) const getUserResponse = await client.send(getUserCommand); return true; } catch (err) { if (err.name === "UserNotFoundException") { return false; } else { console.error("Unexpected error when verifying existence of user", err) throw err; } } } Depending on your requirements, you may want to redirect the user to the dedicated sign-up process, which will result in account creation, and continue later. In my case, it was sufficient to simply create a new user: const createUserCommand = new AdminCreateUserCommand({ Username: username, UserPoolId: USER_POOL_ID, MessageAction: "SUPPRESS" }) const userCreateResponse = await client.send(createUserCommand); Pay attention to the value of the MessageAction property. By default, Cognito will automatically initiate the user verification flow, resulting in a welcome message being sent to the user. I didn't want that, so I decided to suppress it. Trigger email In this demo, the user will receive a verification code via email, which must be manually copied and entered into the application. If you would like to include a hyperlink that, for example, a mobile application can open and handle automatically, you will need to set up SES, which is beyond the scope of this post. const initAuthCommand = new InitiateAuthCommand({ AuthFlow: "USER_AUTH", ClientId: CLIENT_ID, AuthParameters: {"USERNAME": username, "PREFERRED_CHALLENGE": "EMAIL_OTP"}, }) const initResult = await client.send(initAuthCommand); Obtain access token To receive an access token, we need to exchange the one-time password. This is done by responding to an authentication challenge: const challengeCommand = new RespondToAuthChallengeCommand({ ChallengeName: "EMAIL_OTP", ClientId: CLIENT_ID, Session: initResult.Session, ChallengeResponses: { "EMAIL_OTP_CODE": code, "USERNAME": username } }) const { AuthenticationResult } = await client.send(challengeCommand) console.log(AuthenticationResult.AccessToken, AuthenticationResult.RefreshToken) That's it! The AuthenticationResult object contains the AccessToken and RefreshToken, which should now be used just like in any other application that uses tokens for authentication. Summary As we all know, typing a password on a phone is something that nobody enjoys. I hope this post has provided you with insight into how a more convenient login flow can be implemented using Cognito for authentication management.
Intro
I'm going to skip the introduction and description of what Cognito is and why it's useful—I’ll assume you have a reason for reading this.
I’ll use Node.js with AWS SDK V3 for the code snippets, but the language doesn’t matter—you should easily find equivalents in other SDKs.
Please note that this is not a ready-to-use solution you can copy-paste into your app. Instead, think of it as a set of building blocks you can use to design an authentication flow that meets your specific requirements.
This post is based on the latest updates in Cognito (as of around November 2024), which no longer require implementing custom challenges. So, let’s get started!
Setup user pool
The first thing you need is a user pool. Setting up passwordless login is quite straightforward. In the pool's overview, you should see a tile similar to the one shown below:
Next, navigate to the configuration for choice-based sign-in:
Enable one-time password (OTP) by selecting the appropriate checkbox:
For basic usage (perhaps for evaluation purposes), simply select Send email with Cognito and click Save changes:
For more advanced use cases, setting up Amazon SES is required. However, this topic will not be covered in this post.
Setup sdk
We need the aws-sdk library to interact with AWS services:
npm install @aws-sdk/client-cognito-identity-provider --save
Once the SDK is downloaded, set up the client. The snippet below includes all the imports used throughout this post:
import {
CognitoIdentityProviderClient,
InitiateAuthCommand,
RespondToAuthChallengeCommand,
AdminGetUserCommand,
AdminCreateUserCommand
} from "@aws-sdk/client-cognito-identity-provider";
const client = new CognitoIdentityProviderClient({region: "eu-north-1"});
Check existence of user
Cognito sends emails only to users that exist in the pool. Therefore, we must first check if this is the case. There is no need to verify the contents of the response, as the call will throw an exception for a non-existing user:
async function isExistingUser(username) {
try {
const getUserCommand = new AdminGetUserCommand({Username: username, UserPoolId: USER_POOL_ID})
const getUserResponse = await client.send(getUserCommand);
return true;
} catch (err) {
if (err.name === "UserNotFoundException") {
return false;
} else {
console.error("Unexpected error when verifying existence of user", err)
throw err;
}
}
}
Depending on your requirements, you may want to redirect the user to the dedicated sign-up process, which will result in account creation, and continue later. In my case, it was sufficient to simply create a new user:
const createUserCommand = new AdminCreateUserCommand({
Username: username,
UserPoolId: USER_POOL_ID,
MessageAction: "SUPPRESS"
})
const userCreateResponse = await client.send(createUserCommand);
Pay attention to the value of the MessageAction property. By default, Cognito will automatically initiate the user verification flow, resulting in a welcome message being sent to the user. I didn't want that, so I decided to suppress it.
Trigger email
In this demo, the user will receive a verification code via email, which must be manually copied and entered into the application. If you would like to include a hyperlink that, for example, a mobile application can open and handle automatically, you will need to set up SES, which is beyond the scope of this post.
const initAuthCommand = new InitiateAuthCommand({
AuthFlow: "USER_AUTH",
ClientId: CLIENT_ID,
AuthParameters: {"USERNAME": username, "PREFERRED_CHALLENGE": "EMAIL_OTP"},
})
const initResult = await client.send(initAuthCommand);
Obtain access token
To receive an access token, we need to exchange the one-time password. This is done by responding to an authentication challenge:
const challengeCommand = new RespondToAuthChallengeCommand({
ChallengeName: "EMAIL_OTP",
ClientId: CLIENT_ID,
Session: initResult.Session,
ChallengeResponses: {
"EMAIL_OTP_CODE": code,
"USERNAME": username
}
})
const { AuthenticationResult } = await client.send(challengeCommand)
console.log(AuthenticationResult.AccessToken, AuthenticationResult.RefreshToken)
That's it! The AuthenticationResult
object contains the AccessToken
and RefreshToken
, which should now be used just like in any other application that uses tokens for authentication.
Summary
As we all know, typing a password on a phone is something that nobody enjoys. I hope this post has provided you with insight into how a more convenient login flow can be implemented using Cognito for authentication management.