AWS Cognito + Passkey Flex
This guide will walk you through adding Passkey Flex passkey support to your app's Amazon Cognito user pool (opens in a new tab) authentication flow. This setup leverages Cognito's native integration with Lambdas using its custom auth challenge triggers (opens in a new tab).
Your final workflow will look like this:
- Existing and new users sign up with your app through a Cognito user pool.
- Users can sign in and edit their security settings to add a passkey to their account.
- Users can sign in using a passkey instead of a password.
Prerequisites
- A Passkey Flex app
- An AWS account (opens in a new tab)
- Node 20.x or later
Create your Amazon Cognito User Pool and App Client
To simplify configuration, this guide uses the default values for all steps in the User Pool guided setup except for the following:
- In step 2, choose Optional MFA under "MFA enforcement" and Authenticator Apps under "MFA methods", so you don't have to setup SMS options later.
- In step 4, choose Send email with Cognito under "Email provider", so you don't have to setup SES options.
Here are the full settings used to create the User Pool and App Client.
Create a Passkey Flex app
If you already have a Passkey Flex app and API key, skip to the next section.
Otherwise, sign up for a free Passage account (opens in a new tab), which should take you to the Passage Console upon completion.
- Select Create New App under the corresponding Organization, then select Passkey Flex.
- For the domain, use
http://localhost:8081
. It should match the port of the example web app below. - Once the Flex app is created, take note of the
Application ID
for later. - Create an API key at Settings > Create API key. Copy this value to use later when you set up the Lambdas and example server and app.
Compile Lambda code
Clone the example-cognito (opens in a new tab) repo to bootstrap your Cognito application. It contains the Lambda code as well as a web app to test it.
Compile the TypeScript Lambda code and dependencies into three zipped JavaScript artifacts to use in the next step:
npm install
npm run build:lambdas
You should now see the three .zip files in the root of the repo:
.
├── LICENSE
├── README.md
├── app
├── create-auth-challenge.zip <--
├── define-auth-challenge.zip <--
├── lambdas
├── node_modules
├── package-lock.json
├── package.json
├── server
└── verify-auth-challenge.zip <--
Add Cognito custom authentication Lambda triggers
You can read more about the Define (opens in a new tab), Create (opens in a new tab), and Verify (opens in a new tab) authentication challenge triggers in AWS's documentation. They add a fork in the authentication process to allow for additional verification steps before issuing or denying a Cognito user pool JWT.
Passage takes advantage of those hooks by creating a transaction for the passkey authentication ceremony in the Create
trigger and verifying the response from the end user in the Verify
trigger.
The Passage code to start a transaction (opens in a new tab) in the Create
trigger
// lambdas/create-auth-challenge/index.ts
const passage = new PassageFlex(passageConfig);
const externalId = event.request.userAttributes.sub;
const passageTransactionId = await passage.createAuthenticateTransaction({ externalId });
event.response.publicChallengeParameters = { passageTransactionId };
The Passage code to verify (opens in a new tab) the sign in response in the Verify
trigger:
// lambdas/verify-auth-challenge/index.ts
const nonce = event.request.challengeAnswer;
const passage = new PassageFlex(passageConfig);
await passage.verifyNonce(nonce);
event.response.answerCorrect = true;
Now, create the Lambdas and upload the .zip files from the previous step:
Create the Define Auth Challenge Lambda
From the AWS Lambda page, use the following properties to create a new Lambda.
- Author from scratch
- Runtime: Node.js 20.x
- Architecture: x86_64
- Execution role: Create a new role with basic Lambda permissions
Then upload the define-auth-challenge.zip
file for the Define Auth Challenge
Lambda code.
Create the Create Auth Challenge Lambda
Using the same process from step 1, create a new Lambda function. Upload the create-auth-challenge.zip
file.
Create the Verify Auth Challenge Lambda
Using the same process from step 1, create a new Lambda function. Upload the verify-auth-challenge.zip
file.
Set Passage credentials
You need to add Passage credentials to the Create Auth Challenge
and Verify Auth Challenge
Lambdas since that is where Passage is starting and finishing the authentication process, as described above.
Set the Passage credentials (PASSAGE_APP_ID
and PASSAGE_API_KEY
) as environmental variables using the Application ID and API key from earlier by selecting Configuration > Environment Variables > Edit
Register the Lambdas
Finally, once you've created and configured all three Lamdas, go back to User Pool > User pool properties.
Select Add Lambda trigger, then select Custom authentication.
Starting with the Define auth challenge trigger type, select the corresponding Lambda you created previously, then add the trigger.
Repeat this for the Create auth challenge and Verify auth challenge response trigger types.
Finish the example repo setup
Finish setting up the example server and web app.
This creates a .env
file in each of the server
and app
folders.
npm run dev
Fill out all of the env vars by grabbing their values from Amazon Cognito and Passage.
# server/.env
PORT=8080
AWS_COGNITO_REGION=
AWS_COGNITO_USER_POOL_ID=
AWS_COGNITO_APP_CLIENT_ID=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
PASSAGE_APP_ID=
PASSAGE_API_KEY=
# app/.env
VITE_SERVER_PORT=8080
VITE_APP_PORT=8081
VITE_AWS_COGNITO_REGION=
VITE_AWS_COGNITO_USER_POOL_ID=
VITE_AWS_COGNITO_APP_CLIENT_ID=
VITE_PASSAGE_APP_ID=
Now just start the example server and app.
npm start
Test the flow
First, test the traditional user registration and password login flow, then try adding a passkey for the user and logging in without a password.
Sign up a Cognito user
- Sign up a new user with any email (can be fake).
- Sign in with your email and password.
- Once signed in, you'll see the details of the access token returned to you by Cognito.
Add a passkey for the Cognito user
To add a passkey, there's a single endpoint (opens in a new tab) on the server to start a Passage transaction where the externalId
is a unique identifier of the Cognito user:
// server/src/index.ts
const transactionId = await passage.createRegisterTransaction({
externalId: sub,
passkeyDisplayName: email,
});
return res.json({ transactionId });
There's also a call (opens in a new tab) to that endpoint to kick off the passkey creation process in the app:
// app/src/components/AddPasskeyButton.tsx
const response = await fetch(`http://localhost:${import.meta.env.VITE_SERVER_PORT}/users/add-passkey`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error('Error adding passkey');
}
const { transactionId } = await response.json();
await Passage.passkey.register(transactionId);
With your signed in Cognito session, select Add a passkey to start the passkey creation process. After you successfully save your passkey, select Sign out.
Sign in with the passkey
To sign in with a passkey, the app makes two calls (opens in a new tab) to Cognito that initiate and verify the authentication request:
// app/src/cognito.ts
export const signInWithPasskey = async (email: string): Promise<AuthResult> => {
const initAuthResult = await cognitoClient.send(
new InitiateAuthCommand({
ClientId: import.meta.env.VITE_AWS_COGNITO_APP_CLIENT_ID,
AuthFlow: AuthFlowType.CUSTOM_AUTH,
AuthParameters: {
USERNAME: email,
},
}),
);
if (!initAuthResult.ChallengeParameters?.passageTransactionId) {
throw new Error('Error starting passkey login');
}
const transactionId = initAuthResult.ChallengeParameters.passageTransactionId;
const nonce = await Passage.passkey.authenticate({ transactionId });
const authChallengeResult = await cognitoClient.send(
new RespondToAuthChallengeCommand({
ClientId: import.meta.env.VITE_AWS_COGNITO_APP_CLIENT_ID,
Session: initAuthResult.Session,
ChallengeName: ChallengeNameType.CUSTOM_CHALLENGE,
ChallengeResponses: {
USERNAME: email,
ANSWER: nonce,
},
}),
);
};
Sign in with your passkey by selecting the "Try signing in with a passkey" link under the form.
Notice you still received a Cognito access token, even when signing in with a passkey from Passage (the sub
, iss
, and scope
are the same). You can also see the record of the authentication attempts in Passkey Console in the Events tab.
You've now successfully integrated Passkey Flex into an AWS Cognito stack.
With just three short Lambdas, two calls from the app, and one endpoint you can now have your users sign in with passkeys in your existing Cognito app.