Implementing passwordless login with Amazon Cognito
Maciej Kucharski

Maciej Kucharski @maczaj

About: Software engineer focused on building solutions primarily using Java, with a passion for diving deep into the tech stack to uncover its full potential.

Location:
Warsaw, Poland
Joined:
Aug 9, 2021

Implementing passwordless login with Amazon Cognito

Publish Date: Jan 16
2 0

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:

Start configuring passwordless login tile

Next, navigate to the configuration for choice-based sign-in:

Choice-based sign-in configuration link

Enable one-time password (OTP) by selecting the appropriate checkbox:

Choice-based sign-in configuration screen

For basic usage (perhaps for evaluation purposes), simply select Send email with Cognito and click Save changes:

Email configuration for Cognito pool

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"});
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Comments 0 total

    Add comment