AWS Cognito has a flexible integration model with support for passwordless authentication flows via Lambda triggers.

For example, this AWS blog post demonstrates how to use Lambda triggers with Amplify to build a passwordless authentication flow with OTP codes sent via email.

This guide will demonstrate how to adapt such passwordless flows to use passkeys instead of email as the authentication method. This can be achieved in just a few lines of code by using Authsignal’s Node.js SDK in the lambdas, as well as our React Native SDK or our Web SDK in the client.

Example apps

This guide references React Native but a similar integration can be just as easily achieved for Web.

You can find the full code examples for both on Github:

User flow

1

The user is prompted to sign up or sign in with their passkey

iOS passkey prompt
Android passkey prompt
2

The app navigates to the home screen where the app displays an access token for the current cognito session

Cognito access token (iOS)
Cognito access token (Android)

Implementation guide

1. Prerequisites

You should first enable passkeys for your Authsignal tenant and ensure that you’ve configured your relying party.

Next, you’ll need to follow these steps to setup apple-app-site-association (iOS) and assetlinks.json (Android) files on your web domain.

2. Installation

Clone the repo on the with-aws-cognito branch:

git clone --branch with-aws-cognito https://github.com/authsignal/react-native-passkey-example

Then install dependencies inside the repo:

cd react-native-passkey-example
yarn install
npx pod-install ios

3. Configuration

To update the backend configuration used by the lambdas, copy this file and rename it from .env.example to .env then update it with your secret key and the appropriate URL for your region.

To update the client configuration used by the app, replace the values in this file with the values for your Cognito user pool and your Authsignal tenant and region

4. The lambda triggers

Deploying the lambda triggers

The example repo contains four lambdas which can be deployed to your AWS environment using serverless framework. To deploy these lambdas, clone the Github repo and run the following command:

cd lambdas
npx serverless deploy --region YOUR_REGION

Connecting the lambda triggers

Once deployed, these lambdas can be connected to your Cognito user pool:

iOS passkey prompt.

Create auth challenge lambda

This lambda uses the Authsignal Node.js SDK to return a short-lived token back to the app which can be passed to the Authsignal React Native SDK to initiate a passkey challenge:

export const handler: CreateAuthChallengeTriggerHandler = async (event) => {
  const userId = event.request.userAttributes.sub;

  const { token } = await authsignal.track({ action: "cognitoAuth", userId });

  event.response.publicChallengeParameters = { token };

  return event;
};

Verify auth challenge response lambda

This lambda takes the result token returned by the Authsignal React Native SDK and passes it to the Authsignal Node.js SDK to validate the result of the challenge:

export const handler: VerifyAuthChallengeResponseTriggerHandler = async (
  event
) => {
  const userId = event.request.userAttributes.sub;
  const token = event.request.challengeAnswer;

  const { state } = await authsignal.validateChallenge({ userId, token });

  event.response.answerCorrect = state === "CHALLENGE_SUCCEEDED";

  return event;
};

Define auth challenge and pre sign up lambdas

These lambdas don’t have any interesting interaction with Authsignal but are required to get things working end-to-end. You can find out more info about what they do in this AWS blog post.

5. The app

Sign up

We use Amplify to begin sign up, which invokes the create auth challenge lambda and receives an initial token as a challengeParam. We pass this initial token to the Authsignal SDK, which presents the native UI to register a new passkey, and then we pass the result token back to Amplify as the challenge answer.

import {Auth} from 'aws-amplify';
import {authsignal} from '../config';
...

let cognitoUser: any;

const onPressSignUp = async () => {
  const signUpParams = {
    username: userName,
    password: Math.random().toString(36).slice(-16) + 'X',
  };

  await Auth.signUp(signUpParams);

  cognitoUser = await Auth.signIn(userName);

  const {token} = cognitoUser.challengeParam;

  const {data} = await authsignal.passkey.signUp({token, userName});

  await Auth.sendCustomChallengeAnswer(cognitoUser, data);
};

Similar to the example in this AWS blog post, a dummy password is randomly generated because Amplify requires one when signing up, but it won’t actually be used.

Sign in

We use Amplify to begin sign in, which invokes the create auth challenge lambda and receives an initial token as a challengeParam. We pass this initial token to the Authsignal SDK, which presents the native UI to authenticate with a passkey, and then we pass the result token back to Amplify as the challenge answer.

import {Auth} from 'aws-amplify';
import {authsignal} from '../config';
...

let cognitoUser: any;

const onPressSignIn = async () => {
  cognitoUser = await Auth.signIn(userName);

  const {token} = cognitoUser.challengeParam;

  const {data} = await authsignal.passkey.signIn({token});

  await Auth.sendCustomChallengeAnswer(cognitoUser, data);
};