This example will demonstrate how to add passkeys to a native mobile app when using AWS Cognito. It uses the Amplify React Native SDK, but if you’re not using Amplify then you can follow a similar approach can be used to our pre-built UI examples which use the AWS SDK client-side or server-side.

Although the example uses our React Native SDK a similar integration can also be achieved using our iOS SDK, Android SDK or Flutter SDK.

Example Repository

Example app using React Native + Amplify + Authsignal.

User flow

Sign up

The user verifies their email via an OTP challenge then is prompted to create a passkey.

Creating a passkey after registration via email OTP

Sign in

The user is prompted to sign in with their passkey without having to input their email.

Signing in with an available passkey

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.

Installation

yarn install
npx pod-install ios

Configuration

Update this file with the config values for your Authsignal tenant and region-specific URL, along with the values for your AWS Cognito user pool.

The AWS lambdas

The app code

Sign up

Code example

Sign up implementation.

1. Call Amplify SDK signUp method

Pass the email as the username.

await signUp({
  username: email,
  password: Math.random().toString(36).slice(-16) + "X", // Dummy value - never used
  options: {
    userAttributes: {
      email,
    },
  },
});

2. Call Amplify SDK signIn method

This step invokes the Create Auth Challenge lambda.

const { nextStep } = await signIn({
  username: email,
  options: {
    authFlowType: "CUSTOM_WITHOUT_SRP",
  },
});

3. Use the Authsignal Mobile SDK to enroll email OTP

Obtain the short-lived Authsignal token returned by the Create Auth Challenge lambda and use it to initiate email OTP enrollment via the Authsignal Mobile SDK.

const token = nextStep.additionalInfo!.token;

await authsignal.setToken(token);

await authsignal.email.enroll({ email });

This step will send the user an email with an OTP code. Once they input the code into the app, we can verify it using the Authsignal Mobile SDK.

const { data, error } = await authsignal.email.verify({ code });

const resultToken = data.token;

The Authsignal Mobile SDK will return a result token which we can pass to our backend lambdas in the next step.

4. Call Amplify SDK confirmSignIn method

This step invokes the Verify Auth Challenge Response lambda.

const { isSignedIn } = await confirmSignIn({
  challengeResponse: resultToken,
});

Sign in with passkey

Code example

Sign in implementation.

1. Call Authsignal SDK passkey.signIn method

const { data } = await authsignal.passkey.signIn({
  action: "cognitoAuth",
});

2. Call Amplify SDK signIn and confirmSignIn methods

Pass the username and token returned by the Authsignal SDK.

await signIn({
  username: data.username,
  options: {
    authFlowType: "CUSTOM_WITHOUT_SRP",
  },
});

const { isSignedIn } = await confirmSignIn({
  challengeResponse: data.token,
});

Optimizing UX for edge cases

Our sign-in approach above optimizes for the happy path where a user has a passkey available on their device. But it can also handle the scenario where a registered user doesn’t have a passkey available - for example, if they have deleted their existing passkey, or if they’ve switched to a new device on a different platform where their existing passkey can’t be synced.

Falling back to email OTP when no passkey is available

This scenario is handled by checking the errorCode returned by the Authsignal SDK passkey.signIn method:

if (errorCode === ErrorCode.user_canceled || errorCode === ErrorCode.no_credential) {
  return navigation.navigate("SignInEmail");
}

On iOS if no passkey is available the Authsignal SDK will return the user_canceled error code, because iOS treats the absence of a passkey credential as a cancellation event if preferImmediatelyAvailableCredentials is set to true.

On Android if no passkey is available the Authsignal SDK will return the no_credential error code, because Android returns a more explicit error in this scenario.

To keep the behavior consistent on both platforms, we treat cancellation the same as not having a credential available and fall back to presenting an email OTP sign-in option in each case.

Falling back to email OTP when the user cancels passkey sign-in

This approach allows us to optimize sign-in for the happy path while also achieving a good UX for the edge case where a user can’t use passkeys.