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 code

You can find the full code example on Github.

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

This example uses the pre-built UI to present the email OTP challenge but you can also refer to this branch for a fully native implementation which uses our Client API.

Sign in

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

Signing in with a 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

Create Auth Challenge lambda

You can find the full lambda code for this example here.

Verify Auth Challenge Response lambda

You can find the full lambda code for this example here.

The app code

Sign up

You can find a full example of the sign up implementation here.

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. Launch Authsignal pre-built UI

Pass the url returned by the Create Auth Challenge lambda.

const url = nextStep.additionalInfo.url;

const token = await launch(url);

4. Call Amplify SDK confirmSignIn method

This step invokes the Verify Auth Challenge Response lambda.

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

Sign in

You can find a full example of the sign in implementation here.

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.passkeySignInCanceled ||
  errorCode === ErrorCode.noPasskeyCredentialAvailable
) {
  return navigation.navigate("SignInEmail");
}

On iOS if no passkey is available the Authsignal SDK will return the passkeySignInCanceled 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 noPasskeyCredentialAvailable 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.