---
url: 'https://www.corbado.com/blog/passkeys-amazon-cognito'
title: 'How to Add Passkeys to Amazon Cognito'
description: 'This tutorial shows how to integrate passkeys with Amazon Cognito. Upgrade your authentication in AWS systems for your existing users.'
lang: 'en'
author: 'Vincent'
date: '2023-05-04T00:00:00.000Z'
lastModified: '2026-03-25T10:00:09.593Z'
keywords: 'Amazon Cognito, Cognito, AWS Cognito, AWS, Amazon Web Services, Amazon Web Services Passkeys, passkeys in cognito'
category: 'Passkeys Implementation'
---

# How to Add Passkeys to Amazon Cognito

## Overview: AWS Cognito Passkeys

In this blog post, we will cover how to **integrate passkey into an existing Amazon
Cognito instance** that currently authenticates its users with passwords (to get a
**passwordless AWS Cognito** app).

After the first publishing of this tutorial, AWS released their own **native passkeys for
Cognito**. While there are situations where you can make use of the onboard passkeys from
Cognito, there are different other reasons that speak to use a dedicated passkey-first
solution by an passkey specialist such as Corbado.

> See also what AWS says it its blog on
> [optimizing passkey adoption with Amazon Cognito and Corbado](https://aws.amazon.com/blogs/apn/maximizing-passkey-adoption-with-amazon-cognito-and-corbado/)

The frontend of the sample application uses [Angular](https://www.corbado.com/blog/angular-passkeys), while the
backend runs on [Node.js](https://www.corbado.com/blog/nodejs-passkeys) / Express both written in TypeScript. In
general, you can use **any web tech stack to add passkeys to Amazon Cognito**.

[Watch on YouTube](https://www.youtube.com/watch?v=YOB0h4ggKGs)

We use Corbado's passkey-first UI components to handle authentication, while keeping
Amazon Cognito as core user management system in place. Thereby, we leverage AWS Lambda
functions for custom authentication flows in Amazon Cognito.

See the final repository on [GitHub](https://github.com/corbado/passkeys-amazon-cognito).

_Disclaimer: the main purpose of this tutorial is the integration of passkey
authentication in Amazon Cognito and build a working prototype. Session management / API
endpoint authorization is only covered on a high-level and needs to be adapted for further
usage. Note that some settings need to be made in Amazon Cognito to make the example work
out-of-the-box. Take a look at_[ _1. Setup of Amazon Cognito_](#1-setup-of-amazon-cognito)
_and_[ _3. Setup AWS Lambda functions with custom auth flows_](#3-setup-aws-lambda-functions-with-custom-auth-flows)
_to check these settings. These AWS Lambda functions are used by Corbado to hook into the
authentication flow, as Amazon Cognito heavily relies on passwords (there's not even a way
to export password hashes), so a fully passwordless setup out-of-the-box is otherwise not
possible in Amazon Cognito._

## Cognito Passkeys vs. Corbado Passkeys

After the initial publishing of this blog post, Cognito introduced a more elaborated
version of their native Cogntio passkey implemetation. There are definitely scenarios
where these native Cognito passkeys make more sense to be used. However, in some other
scenarios going with Corbado's passkey implementation is more useful. In the following, I
want to highlight the key differences that should help you make a choice to better find
out which solution is the right one for your use case.

![cognito passkeys corbado comparison](https://www.corbado.com/website-assets/cognito_passkeys_corbado_comparison_e46ec634f6.png)

### Solution

In general, Amazon Cognito is a general-purpose identity provider (IdP) that offers a full
authentication & user management suite. It has many features and can serve as the backbone
of a digital product. Its features span widely across authentication.

Corbado, on the other hand, is a frontend-focused passkey layer designed to drive high
[passkey adoption](https://www.corbado.com/blog/passkey-adoption-business-case). It comes with a suite of
enterprise [passkey features](https://www.corbado.com/blog/social-logins-pre-filled-passkeys-customization).

### Problem Focus

The core problem that Cognito solves is mainly authenticating (and managing) users.

In contrast, Corbado focuses on addressing the pain users face with traditional MFA, such
as SMS OTPs and [authenticator](https://www.corbado.com/glossary/authenticator) apps. This ultimately results in
lower operational costs, including reduced SMS OTP fees, fewer password / MFA recovery
support efforts and a significant reduction in [phishing](https://www.corbado.com/glossary/phishing) and fraud
risks.

### Passkey Approach

In AWS Cognito, passkeys are just one of many authentication methods and Amazon Cognito
does not actively encourage users to adopt passkeys. Users must manually create and use a
passkey, e.g. by clicking on a separate
[passkey button](https://www.corbado.com/blog/passkey-login-best-practices) on a login screen. From the data we
have analyzed, this results in
[low passkey adoption](https://www.corbado.com/faq/challenges-of-low-passkey-adoption), as primarily power users
are familiar with the process. Regular users will tend to keep their behavior and will
continue to use passwords.

Corbado, however, is passkey-first because we firmly believe the future will be
passkeys-only. Therefore, we focus on delivering automatic passkey logins that eliminate
user friction ("Don't make the user think"), optimizing all passkey UX flows and copy to
be customer-first. Additionally, we offer large organizations a risk-free approach to
achieving ROI, securing their users and handling technical complexity and rollout risks
that come with passkeys. This approach results in significantly higher
[passkey adoption](https://www.corbado.com/blog/passkey-adoption-business-case) rates.

### Ideal Customer Profile

Amazon Cognito's passkeys are a viable alternative, especially if you already use Amazon’s
hosted frontend. In this case, you don't need to build the passkey frontend / UI yourself.
We typically see this approach being adopted by SMBs or organizations without an existing
user base, as AWS passkeys can be relatively simple to implement when integration
complexities are minimal.

Corbado, however, is ideal for customers who have already built a custom frontend on top
of Cognito and are not using Cognito’s hosted UI. Large deployments with many existing
users benefit from Corbado's solution, as it simplifies handling the complexity of device
and user variety (in these deployments, you often have to deal with thousands of different
device, OS and browser version combinations - each with their own specifics and
characteristics).

### Technical Integration

Amazon Cognito is a standalone IdP that does not necessarily require integration with
anything else to offer passkeys.

Corbado, on the other hand, is complementary to Cognito or other IdPs. As a passkey
enterprise layer, it requires an underlying IdP such as Cognito, an in-house
implementation or another IDP like [Keycloak](https://www.corbado.com/blog/keycloak-passkeys).

As you've seen, both passkey options - Amazon Cognito passkeys and
[Corbado passkeys](https://www.corbado.com/blog/passkey-performance-testing) - have their place and serve
different purposes. If you're interested in passkeys for large customer bases, feel free
to [reach out](https://www.corbado.com/contact) and get more resources, such as passkey strategies, enterprise
guides and data on rollout and adoption.

## Quickstart

If you want to see the results directly, we provide a docker file. You only need to add
the required environment variables and can jump start with:

```bash
docker-compose up
```

Note that you need to provide Amazon Cognito and Corbado environment variables to get
things working. Besides, you may also need to copy the AWS CLI credentials from
.aws/credentials (see docker-compose.yml).

## 1. Setup of Amazon Cognito

If you have already setup Amazon Cognito, you can skip this step and go directly to
[step 2](#2-current-password-based-auth-with-amazon-cognito).

We set up a basic user pool in Amazon Cognito with the following properties (most are
default). We use this setup because most of the current Amazon Cognito implementations
weve seen are configured this way. We would generally recommend higher security levels.

- Authentication with email and passwords
- No federated identity providers or social logins
- Default password policy
- No MFA
- Self-service account recovery
- Self-service sign-up
- No custom attributes
- Email handling by Cognito / AWS SES
- Confidential client (as authentication will be handled by our backend in
  [Node.js](https://www.corbado.com/blog/nodejs-passkeys) / Express)

![Setup of Amazon Cognito Landing Page](https://www.corbado.com/website-assets/64589886aae4644010fdea69_setup_of_amazon_cognito_1_b65f1abf55.png)

Select "Email" as sign-in option:

![Setup of Amazon Cognito Configure Sign-in Experience](https://www.corbado.com/website-assets/64ef497c2390d5531bb5ad3e_setup_of_amazon_cognito_2_c1ed228b7a.png)

Define the password, [MFA ](https://www.corbado.com/blog/psd2-passkeys/are-passkeys-two-factor-authentication)and
recovery settings:

![Setup of Amazon Cognito Configure security requirements](https://www.corbado.com/website-assets/64ef49a1d3ee34250845decc_setup_of_amazon_cognito_3_9019af7b48.png)

![Setup of Amazon Cognito configure security requirements](https://www.corbado.com/website-assets/64ef49b2cb06228971fb98dd_setup_of_amazon_cognito_4_3bb4f4c638.png)

Configure the sign-up experience:

![Setup of Amazon Cognito configure sign-up experience](https://www.corbado.com/website-assets/64ef49bca6c288afdc698445_setup_of_amazon_cognito_5_14a34c9de1.png)

![Setup of Amazon Cognito configure sign-up experience](https://www.corbado.com/website-assets/64ef49c71e5caf03a29f6bc4_setup_of_amazon_cognito_6_30ffa49e31.png)

To keep things simple, just use the Amazon Cognito email service:

![Setup of Amazon Cognito configure message delivery](https://www.corbado.com/website-assets/64ef49d51e5caf03a29f79f3_setup_of_amazon_cognito_7_f71b10e75f.png)

Define a user pool name:

![Setup of Amazon Cognito Integrate your app](https://www.corbado.com/website-assets/64ef49e5e9ed29ebd0479c8b_setup_of_amazon_cognito_8_f8a997ad3f.png)

Select "Confidential client" as requests to Amazon Cognito will be made via our
[Node.js](https://www.corbado.com/blog/nodejs-passkeys) backend:

![Setup of Amazon Cognito Integrate your app](https://www.corbado.com/website-assets/64ef49f16efe1b0325baefee_setup_of_amazon_cognito_9_de363b826b.png)

Review everything and create the user pool:

![Setup of Amazon Cognito review and create](https://www.corbado.com/website-assets/64ef49fd067697f0b922156f_setup_of_amazon_cognito_10_f058bf4b14.png)

![Setup of Amazon Cognito review and create](https://www.corbado.com/website-assets/64ef4a0a46e4536af0479570_setup_of_amazon_cognito_11_66630b2c27.png)

![Setup of Amazon Cognito review and create](https://www.corbado.com/website-assets/64ef4a148f454931966da763_setup_of_amazon_cognito_12_9682c13d38.png)

![Setup of Amazon Cognito review and create](https://www.corbado.com/website-assets/64ef4a1fbc214f77a5e46449_setup_of_amazon_cognito_13_7e5081ee39.png)

![Setup of Amazon Cognito review and create](https://www.corbado.com/website-assets/64ef4a2951cba9d9c02877a4_setup_of_amazon_cognito_14_fcb7dfb0cc.png)

## 2. Current password-based auth with Amazon Cognito

### 2.1 Frontend in Angular

The current frontend is a simple web application that has a login / sign-up view and a
logged-in view.

The structure of the frontend code follows a typical [Angular](https://www.corbado.com/blog/angular-passkeys)
project structure (only most important files are described below, see
[GitHub repository](https://github.com/corbado/passkeys-amazon-cognito) for full code):

```txt filename="frontend-structure"
.
├── src
|   ├── app
|   |   ├── auth
|   |   |   ├── auth.component.html       # HTML structure for login/sign-up view with email and password input
|   |   |   ├── auth.component.ts         # Interacts with AuthService to login and sign-up
|   |   ├── ...
|   ├── logged-in
|   |   ├── logged-in.component.html      # HTML structure for logged-in view with main function to logout
|   |   ├── logged-in.component.ts        # Logout and go back to login/sign-up view
|   ├── app-routing.module.ts             # Routing
|   ├── app.component.html                # Display the page that is currently routed to
|   ├── auth.service.ts                   # Interaction with the backend to sign-up, login and logout
|   ├── ...
|   ├── assets
|   ├── index.html
|   ├── main.ts
|   ├── ...
├── ...
```

The frontend is generated with [Angular](https://www.corbado.com/blog/angular-passkeys) CLI version 15.2.7. If
not done yet, install it via:

```bash
npm install -g @angular/cli
```

Install all other packages by running the following command in the ./frontend- angular
directory:

```bash
ng serve
```

You should see the following screen when you go to
[http://localhost:4200](http://localhost:4200) in your browser:

![Localhost with Password](https://www.corbado.com/website-assets/64589b2303060408b3a7bc3c_frontend_angular_login_signup_2d1e2f95a7.png)

### 2.2 Backend in Node.js / Express

The backends main purpose is to communicate with Amazon Cognito for logging and signing up
users. We use the AWS SDK
[@aws-sdk/client-cognito-identity- provider](https://www.npmjs.com/package/@aws-sdk/client-cognito-identity)
to communicate with Amazon Cognito.

The structure of the backend code follows a typical Node.js / Express project structure
(only most important files are described below, see
[GitHub repository](https://github.com/corbado/passkeys-amazon-cognito) for full code):

```txt filename="backend-structure"
.
├── controllers
|   ├── authCognitoController.ts      # Controller that handles the communication with Amazon Cognito
├── app.ts                             # Our server that handles the routing to the controller
├── ...
```

Install all required packages by running the following command in the
./backend-[nodejs](https://www.corbado.com/blog/nodejs-passkeys) directory:

```bash
npm install
```

Please create a .env file in the ./backend-node.js directory and set the values for
COGNITO_REGION, COGNITO_USER_POOL_ID, COGNITO_CLIENT_ID, COGNITO_CLIENT_SECRET and
COGNITO_JWKS (JWKS stands for JSON Web Key Set and contains keys that are used to verify
JSON Web Tokens, JWTs, in the session management). In docker-compose.yml, we added
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, which are optional here and can also be
provided in other ways.

They can be obtained in the management console in Amazon Cognito:

COGNITO_REGION: Amazon Cognito &gt; Navigation bar

![Amazon Cognito region selection](https://www.corbado.com/website-assets/64ef4a4f176c9b353be3020a_amazon_cognito_region_ad60139e67.png)

COGNITO_USER_POOL_ID: Amazon Cognito &gt; User pools

![Amazon Cognito user pools](https://www.corbado.com/website-assets/64589b6683dff0b7cddc00bf_amazon_cognito_user_pools_5992ef1ad1.png)

COGNITO_CLIENT_ID: Amazon Cognito &gt; User pools &gt; corbado-user-pool &gt; App
integration (on the bottom)

![Amazon Cognito Client ID](https://www.corbado.com/website-assets/64589b7a169fc416aa849f02_amazon_cognito_client_id_9ebfd934f5.png)

COGNITO_CLIENT_SECRET: Amazon Cognito &gt; User pools &gt; corbado-user-pool &gt; App
client: corbado-backend

![Amazon Cognito Client Secret](https://www.corbado.com/website-assets/64589b88c71da135165bd558_amazon_cognito_client_secret_c0411a7c6b.png)

COGNITO_JWKS: Open the following URL in your browser:
`https://cognito- idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json` and
copy the value for keys (should be an array):

![Amazon Cognito JWKs](https://www.corbado.com/website-assets/64ef4a716ea8102db36c4ad9_amazon_cognito_jwks_05388f458a.png)

Start the local development server that runs on port 3000:

```bash
npm run dev
```

If everything works fine, you should see the following output in the terminal:

![Backend Node.js Amazon Cognito](https://www.corbado.com/website-assets/64589bb731db95fd0d46b1e2_backend_nodejs_express_terminal_3230a60ad4.png)

## 3. Setup AWS Lambda functions with custom auth flows

Currently, Cognito is set up with username / email and password as authentication. Coming
up with your own authentication logic or an external authentication provider, like
Corbado, requires custom auth flows in Amazon Cognito. This implies the setup of AWS
Lambda functions to handle the authentication process. AWS Lambda functions are small
blocks of code that can be run in the cloud without requiring the user to manage a server
or infrastructure, and they are designed to respond to specific events or requests. When
an AWS Lambda function is triggered, AWS automatically spins up a compute environment to
run the code, and then shuts it down when the function is finished. This makes it easy to
build scalable, event-driven architectures that are both flexible and cost-effective,
since users only pay for the compute time that their functions use.

Amazon Cognito provides a good explanation of the flow used for custom authentication
[here](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html).

The only change we make in regard to this chart is that the Challenges answers /
respondToAuthChallenge is not really executed, as the authentication with Corbado happened
even before the Lambda functions are triggered. Still, these calls in the code are made,
as they are required by Amazon Cognito.

Moreover, the following requirements apply in our use case:

- Existing users should still be able to log in with their passwords (in case they do not
  want / cannot use passkeys or as a fallback in case of errors).
- New users will follow a passkey-first approach: if possible, they create and use a
  passkey and their default fallback option are passwordless email magic links.
- Session management for all users should still be handled by Amazon Cognito, as two
  systems for session management would require too much maintenance.
- After sign-up, users are automatically logged into the application to improve the
  [conversion rate](https://www.corbado.com/blog/logins-impact-checkout-conversion) (the email confirmation
  should be skipped for now).

### 3.1 Create AWS Lambda functions

Go to your [AWS Lambda console](https://console.aws.amazon.com/lambda/home) and create
three AWS Lambda functions:

![Create Lambda functions](https://www.corbado.com/website-assets/64589cb72835a8c3b64d3f2d_create_lambda_functions_1_d23ddd34cd.png)

![Create Lambda functions](https://www.corbado.com/website-assets/64589cc12608d15e6bb64654_create_lambda_functions_2_3107279620.png)

#### 3.1.1 Define auth challenge function

Create the first function, name it like defineCorbadoAuthChallenge and add the following
code. It basically triggers the custom authentication flow.

```js filename="defineCorbadoAuthChallenge.js"
export const handler = async (event) => {
    // This if statement checks if the event object contains a session array property that is empty. If it is
    // empty, it means that the authentication flow has just started, so the customChallenge function is
    // called to send a custom challenge.
    if (!event.request.session.length) {
        return customChallenge(event);
    }

    // This if statement checks if any of the previous authentication attempts in the session array had a
    // challengeName property that is not equal to "CUSTOM_CHALLENGE". If there was an attempt that
    // used a different challenge type, the deny function is called with an error message.
    if (
        event.request.session.find(
            (attempt) => attempt.challengeName !== "CUSTOM_CHALLENGE",
        )
    ) {
        return deny(event, "Expected CUSTOM_CHALLENGE");
    }

    // This code block handles the authentication response from the client. It checks the last item in the
    // session array to see if the challengeResult property is true. If it is true, the authentication is
    // considered successful, so the allow function is called to allow the user. If the challengeResult is not
    // true, the function checks if the maximum number of failed attempts has been reached
    // (determined by the countAttempts function). If the maximum number has not been reached, the
    // customChallenge function is called to send another custom challenge. Otherwise, the deny function
    // is called to deny the authentication request with an error message.
    const lastResponse = event.request.session.slice(-1)[0];
    if (lastResponse.challengeResult === true) {
        return allow(event);
    } else if (countAttempts(event, false) === 0) {
        return customChallenge(event);
    }
    return deny(event, "Failed to authenticate with passkeys");
};

// These are the helper functions used in the main handler function. deny is called to deny the
// authentication request with an error message. Allow is called to allow the user if the authentication
// is successful. customChallenge is called to send a custom challenge to the user.
function deny(event, reason) {
    event.response.issueTokens = false;
    event.response.failAuthentication = true;
    console.log("Authentication denied!");
    return event;
}

function allow(event) {
    event.response.issueTokens = true;
    event.response.failAuthentication = false;
    console.log("Authentication allowed!");
    return event;
}

function customChallenge(event) {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = "CUSTOM_CHALLENGE";
    return event;
}

// The countAttempts function returns the number of authentication attempts stored in the session array of the event
// object passed to the Lambda function. It optionally filters out any authentication attempts that used
// the PROVIDE_AUTH_PARAMETERS challenge type. This function is used in the handler function to determine if the
// maximum number of failed attempts has been reached, and if so, to deny the authentication request.
function countAttempts(event, excludeProvideAuthParameters = true) {
    if (!excludeProvideAuthParameters) return event.request.session.length;
    return event.request.session.filter(
        (entry) => entry.challengeMetadata !== "PROVIDE_AUTH_PARAMETERS",
    ).length;
}
```

#### 3.1.2 Create auth challenge function

Create the second function, name it like createCorbadoAuthChallenge and add the following
code. This AWS Lambda function is directly triggered after the defineCorbadoAuthChallenge
and creates a custom challenge (we do not make real use of the challenge,
[see above](#setup-aws-lambda-functions)).

```js filename="createCorbadoAuthChallenge.js"
// This function generates a random challenge string and returns a response object containing the challenge string
// and other necessary parameters for the custom authentication flow to proceed.
export const handler = async (event) => {
    const challenge = Math.random().toString(36).substring(7);

    event.response = {
        challengeMetadata: "CORBADO_CHALLENGE_COGNITO_CLAIM",
        // Cognito requires at least one public param
        publicChallengeParameters: {
            challenge: challenge,
        },
        privateChallengeParameters: {
            challenge: challenge,
        },
    };

    return event;
};
```

#### 3.1.3 Verify auth challenge function

Create the third function, name it like verifyCorbadoAuthChallenge and add the following
code. It verifies the response (we not really verify the response, but just verify if the
call and response of the AWS Lambda function are successful).

```js filename="verifyCorbadoAuthChallenge.js"
export const handler = async (event) => {
    if (
        !event.request.privateChallengeParameters ||
        event.request.privateChallengeParameters.challenge !==
            event.request.challengeAnswer
    ) {
        event.response.answerCorrect = false;
        return event;
    }

    console.log("Successfully authenticated!");
    event.response.answerCorrect = true;

    return event;
};
```

Afterwards, you should see three AWS Lambda functions in your AWS Lambda console:

![Create Lambda functions](https://www.corbado.com/website-assets/64589d35c71da138875bf736_create_lambda_functions_3_708ccc5ff7.png)

These AWS Lambda functions are very basic and lean. You can create loggers or work with
console.log() to get a better understanding what is happening under the surface. We also
recommend checking out Amazon CloudWatch as it has good capabilities for logging and
debugging. It might be quite difficult to get comfortable with it at first, but once you
understand it, it's easy to handle:

Open Log groups and click on the AWS Lambda function you want to check:

![Amazon Cloudwatch](https://www.corbado.com/website-assets/64589f857a0ca03258efeedb_cloudwatch_1_1d697dece6.png)

Click on the log stream where you want to see details:

![Amazon Cloudwatch](https://www.corbado.com/website-assets/64ef4a92176c9b353be360dc_cloudwatch_2_4984bd216b.png)

Now you can see the log events:

![Amazon Cloudwatch](https://www.corbado.com/website-assets/64ef4a9fe63f78db8ac882a0_cloudwatch_3_7bdee8810a.png)

### 3.2 Add AWS Lambda triggers to user pool in Amazon Cognito

Add an AWS Lambda trigger at Amazon Cognito &gt; User pools &gt; Corbado-user-pool &gt;
User pool properties:

![Add Lambda Triggers](https://www.corbado.com/website-assets/64ef4ab2aa6f62041ab725b3_add_lambda_triggers_1_771bbf4458.png)

Click on Add Lambda trigger

- Select Custom authentication
- Select Define auth challenge
- Select defineCorbadoAuthChallenge

![Add Lambda Triggers](https://www.corbado.com/website-assets/64ef4ac8d74a05062b53fbdc_add_lambda_triggers_2_4590a3013f.png)

Create two additional Lambda triggers for the two remaining Lambda functions in the same
way:

1. Create auth challenge -&gt; createCorbadoAuthChallenge
2. Verify auth challenge response -&gt; verifyCorbadoAuthChallenge

### 3.3 Define new custom attribute "createdByCorbado"

To find out if a user was created via Corbado or via the old Amazon Cognito sign-up
component, we add a custom attribute createdByCorbado to Amazon Cognito. It is needed to
identify users who could fall back to a password they might know, or offer email magic
links to all other users, who have never set or seen a password.

Select your user pool:

![Define custom attribute](https://www.corbado.com/website-assets/6458a07c4a2367fec0a39884_define_custom_attribute_1_5646d10997.png)

Click on "Sign-up experience":

![Define custom attribute](https://www.corbado.com/website-assets/6458a09528a7f35608228858_define_custom_attribute_2_4a40929cc0.png)

Click on "Add custom attributes":

![Define custom attribute](https://www.corbado.com/website-assets/6458a0a6947864dc898feac3_define_custom_attribute_3_b6b2bcf2b5.png)

Add the new attribute with name createdByCorbado, leave all other setting on default and
click on Save changes (unfortunately, theres no Boolean datatype).

![Define custom attribute](https://www.corbado.com/website-assets/64ef4aefd16bd7230b9c9fb2_define_custom_attribute_4_a4576c560c.png)

## 4. Integrate Corbado into your application

Now everything is set up to add Corbado to the app. The flow looks as follows:

1. A new user signs-up or an existing user logs into the Corbado web component and creates
   a passkey (passkey and device management is handled by Corbado).
2. The user is created / checked in Amazon Cognito.
3. AWS Lambda functions handle the custom auth flow and generate a session for the user.
4. The user is logged in and redirected.

Currently, the issue with Amazon Cognito is that it heavily relies on passwords. For new
users, you need to setup a password, otherwise they neither can be created nor sessions
can be generated. Therefore, we generate a random password that the user never sees and
store the hashed and salted version in Amazon Cognito. Further, we automatically confirm
the user account via our backend.

The following scenarios are covered by our integration:

1. **Existing password-based users created prior to passkeys integration want to log in:**
   We let them log in with their passwords and offer them to create passkey after their
   first login. Afterwards, passkeys are leveraged as preferred login method, but the
   password can still be used as a fallback login method.
2. **New users with passkey-ready devices:** We
   [create a passkey](https://www.corbado.com/blog/passkey-creation-best-practices) for them directly in the
   first step and will use this passkey for subsequent logins. As fallback method, email
   magic links are used.
3. **New users with non-passkey-ready devices:** To stay
   [future-proof](https://www.corbado.com/faq/are-passkeys-the-future), we offer passwordless email magic links
   as authentication method.

### 4.1 Setup in Corbado developer panel

We start by heading over to the Corbado developer panel and create an account. We are
welcomed by this screen:

![Corbado developer panel Getting started](https://www.corbado.com/website-assets/6458a16283dff02678dc5a29_corbado_developer_panel_1_498c9a1c4f.png)

We click on Integration guide to do everything step by step:

![Corbado developer panel integration guide](https://www.corbado.com/website-assets/64ef4b05037a7d053ee28cc4_corbado_developer_panel_2_347e1f37c5.png)

We integrate via Web component, so that's what we select.

![Corbado developer panel integration guide](https://www.corbado.com/website-assets/6458a1872835a81e444d8b2b_corbado_developer_panel_3_37f2467f84.png)

Also, we have a system with an existing user base, so we click 'Yes'. Afterwards, we find
ourselves at the overview of the developer panel (you need to confirm your account via
email if it's your first time).

Head over to Getting started &gt; Integration guide. This guides us through all the steps
necessary for the integration to work.

![Corbado developer panel getting started integration guide](https://www.corbado.com/website-assets/64ef4b1dd16bd7230b9cd620_corbado_developer_panel_4_300698a19b.png)

In step 1, we create an API secret. We need the project ID and the generated API secret
later in order to communicate with Corbado's API. Also, we need to configure our
authorized origin. The authorized origin is the browser URL from the site where the web
component is integrated (with protocol and port but without path). We need it for CORS
([cross-origin](https://www.corbado.com/blog/iframe-passkeys-webauthn) request sharing). In our case, we set it
to [http://localhost:4200](http://localhost:4200).

![Corbado developer panel congiure authentication to Corbado](https://www.corbado.com/website-assets/64ef4b292bc0f759b22768b7_corbado_developer_panel_5_e9f79e12bf.png)

In the second, optional step, we configure the webhook. This is needed, so Corbado can
communicate with our backend, e.g. for checking if a username and password of an existing
user match. More details on that later.

We will later set up our webhook at
[http://localhost:3000/api/corbado/webhook](http://localhost:3000/api/corbado/webhook)
with a webhook username and webhook password as credentials, so we can already enter that
here.

![Corbado developer panel Settings Webhooks](https://www.corbado.com/website-assets/64ef4b36f5c30215f9cfadb6_corbado_developer_panel_6_2cdf292c3c.png)

In step 3, we add our Application URL, Redirect URL and
[Relying Party](https://www.corbado.com/glossary/relying-party) ID. The Application URL is the URL in the
frontend where the web component runs. For example, it's used to forward users to the web
component again after they clicked on an email magic link.

The Redirect URL is the URL that receives Corbados auth token as GET parameter, after
Corbado has successfully authenticated a user, so that a session can be started. We
implement our Redirect URL at [http://localhost:4200/auth](http://localhost:4200/auth)
later, but we can enter it here already.

The [Relying Party](https://www.corbado.com/glossary/relying-party) ID is the domain where we bind our passkeys
to. The domain in the browser where a passkey is used must be a matching domain to the
[Relying Party](https://www.corbado.com/glossary/relying-party) ID. As we test locally, we set the Relying Party
ID to localhost.

![Corbado developer panel Application URL Redirect URL Relying Party ID](https://www.corbado.com/website-assets/64ef4b44cb85751c2ceb8432_corbado_developer_panel_7_e322f8c33a.png)

Just like that, the project in Corbado is set up!

### 4.2 Frontend integration

We add the Corbado script into our HTML head in index.html:

```html filename="index.html"
<!doctype html>
<html lang="en">
    <head> </head>
    <body>
        <script src="https://<Project ID>.frontendapi.corbado.io/auth.js"></script>
        <app-root></app-root>
    </body>
</html>
```

Then, we replace the existing login / sign-up components with the Corbado web component.

```html filename="auth.component.html"
<corbado-auth project-id="<Project ID>" conditional="yes">
    <input name="username" id="corbado-username" required autocomplete="webauthn" />
</corbado-auth>
```

Next, we add the CUSTOM_ELEMENTS_SCHEMA to app.module.ts.

```ts filename="app.module.ts"
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { AuthComponent } from "./auth/auth.component";
import { LoggedInComponent } from "./logged-in/logged-in.component";

@NgModule({
    declarations: [AppComponent, AuthComponent, LoggedInComponent],
    imports: [BrowserModule, FormsModule, AppRoutingModule, HttpClientModule],
    providers: [],
    bootstrap: [AppComponent],
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
```

Entering [http://localhost:4200](http://localhost:4200) in the browser should give us this
view:

![Web component frontend passkey integration](https://www.corbado.com/website-assets/6458a20cc00dd7bde5373530_frontend_integration_1ef72080b1.png)

To handle the Redirect URL and send the corbadoAuthToken from the Redirect URL to our
backend for verification, we add the following code to auth.component.ts:

```ts filename="auth.component.ts"
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { AuthService } from "../auth.service";
import { Subscription } from "rxjs";

@Component({
    selector: "app-auth",
    templateUrl: "./auth.component.html",
    styleUrls: ["./auth.component.scss"],
})
export class AuthComponent implements OnInit {
    email = "";
    password = "";
    errorMessage = "";
    queryParamsSubscription!: Subscription;

    constructor(
        private authService: AuthService,
        private router: Router,
        private route: ActivatedRoute,
    ) {
        this.queryParamsSubscription = this.route.queryParams.subscribe((queryParams) => {
            if (queryParams["corbadoAuthToken"] != undefined) {
                let corbadoAuthToken = queryParams["corbadoAuthToken"];
                this.authService
                    .corbadoAuthTokenValidate(corbadoAuthToken)
                    .then((res) => {
                        router.navigate(["/logged-in"]);
                    })
                    .catch((error) => console.log(error));
            }
        });
    }

    ngOnInit(): void {}

    // ...
}
```

Lastly, we add the corbadoAuthTokenValidate function to our AuthService:

```ts filename="auth.service.ts"
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Router } from "@angular/router";
import { map, Observable, Subject } from "rxjs";

@Injectable({
    providedIn: "root",
})
export class AuthService {
    private isAuthenticated = false;
    private authStatusListener = new Subject<boolean>();
    private token: string = "";
    private userId: string = "";

    constructor(
        private http: HttpClient,
        private router: Router,
    ) {}

    getToken() {
        return this.token;
    }

    getUserId() {
        return this.userId;
    }

    getAuthStatusListener() {
        return this.authStatusListener.asObservable();
    }

    // ...

    logout() {
        this.token = "";
        this.isAuthenticated = false;
        this.authStatusListener.next(false);
        this.router.navigate(["/login"]);
    }

    getIsAuth() {
        return this.isAuthenticated;
    }

    async corbadoAuthTokenValidate(corbadoAuthToken: string) {
        try {
            this.http
                .get<{
                    idToken: string;
                }>(
                    `http://localhost:3000/api/corbado/authTokenValidate?corbadoAuthToken=${corbadoAuthToken}`,
                )
                .subscribe((responseData) => {
                    const token = responseData.idToken;
                    this.token = token;
                    if (token) {
                        this.isAuthenticated = true;
                        this.authStatusListener.next(true);
                        this.router.navigate(["/logged-in"]);
                    }
                });
        } catch (error) {
            console.error(error);
            throw error;
        }
    }
}
```

### 4.3 Backend integration

At first, we add Corbado environment variables to our .env file:

- CORBADO_PROJECT_ID: obtain it from
  [here](https://app.corbado.com/app/settings/general/project-info)
- CORBADO_API_SECRET: obtain it from
  [here](https://app.corbado.com/app/settings/credentials/api-keys)
- CORBADO_WEBHOOK_USERNAME: obtain it from
  [here](https://app.corbado.com/app/settings/webhooks)
- CORBADO_WEBHOOK_PASSWORD: obtain it from
  [here](https://app.corbado.com/app/settings/webhooks)
- (Optional) CORBADO_CLI_SECRET: obtain it from
  [here](https://app.corbado.com/app/settings/credentials/cli-secret)

Then, we install the required Corbado packages with:

```bash
npm install corbado corbado-webhook --save
```

Next, we create a new controller authCorbadoController in the backend. It handles

- authTokenValidate: It receives the corbadoAuthToken from the frontend and verifies it at
  the Corbado API. Moreover, we check if the user exists in Amazon Cognito and create him
  if he does not so. Eventually, we create the Amazon Cognito session.
- handleWebhook: It checks if a user exists in Amazon Cognito already and handles the
  password authentication for existing users.

```ts filename="authCorbadoController.ts"
import { Request, Response } from "express";
// @ts-ignore
import crypto from "crypto";
// @ts-ignore
import jwt from "jsonwebtoken";
import {
    verifyPassword,
    getUserStatus,
    createUser,
    createSession,
} from "./authCognitoController";
import { NOT_EXISTS } from "../utils/constants";
const Corbado = require("@corbado/node-sdk");

require("dotenv").config({ path: "../.env" });

// Corbado Node.js SDK
const CORBADO_PROJECT_ID = process.env.CORBADO_PROJECT_ID;
const CORBADO_API_SECRET = process.env.CORBADO_API_SECRET;
const config = new Corbado.Configuration(CORBADO_PROJECT_ID, CORBADO_API_SECRET);
const corbado = new Corbado.SDK(config);

export const handleWebhook = async (req: Request, res: Response) => {
    try {
        // Get the webhook action and act accordingly. Every Corbado
        // webhook has an action.

        let request: any;
        let response: any;
        console.log("BEFORE ACTION");
        switch (corbado.webhooks.getAction(req)) {
            // Handle the "authMethods" action which basically checks
            // if a user exists on your side/in your database.
            case corbado.webhooks.WEBHOOK_ACTION.AUTH_METHODS: {
                console.log("WEBHOOK AUTH METHODS");
                request = corbado.webhooks.getAuthMethodsRequest(req);

                // Now check if the given user/username exists in your
                // database and send status. Implement getUserStatus()
                // function below.#
                console.log("BEFORE USER STATUS");

                const status = await getUserStatus(request.data.username);
                let correctUserStatus = status.userStatus;
                if (status.createdByCorbado) {
                    correctUserStatus = "not_exists";
                }
                response = corbado.webhooks.getAuthMethodsResponse(correctUserStatus);
                res.json(response);
                break;
            }

            // Handle the "passwordVerify" action which basically checks
            // if the given username and password are valid.
            case corbado.webhooks.WEBHOOK_ACTION.PASSWORD_VERIFY: {
                console.log("WEBHOOK PASSWORD VERIFY");
                request = corbado.webhooks.getPasswordVerifyRequest(req);

                // Now check if the given username and password is
                // valid. Implement verifyPassword() function below.
                const isValid = await verifyPassword(
                    request.data.username,
                    request.data.password,
                );
                response = corbado.webhooks.getPasswordVerifyResponse(isValid);
                res.json(response);
                break;
            }
            default: {
                res.status(400).send("Bad Request");
                return;
            }
        }
    } catch (error: any) {
        // We expose the full error message here. Usually you would
        // not do this (security!) but in this case Corbado is the
        // only consumer of your webhook. The error message gets
        // logged at Corbado and helps you and us debugging your
        // webhook.
        console.log(error);

        // If something went wrong just return HTTP status
        // code 500. For successful requests Corbado always
        // expects HTTP status code 200. Everything else
        // will be treated as error.

        res.status(500).send(error.message);
        return;
    }
};

export const authTokenValidate = async (req: Request, res: Response) => {
    console.log("AUTH TOKEN VALIDATE STARTED");

    try {
        let corbadoAuthToken = req.query["corbadoAuthToken"] as string;
        let clientInfo = corbado.utils.getClientInfo(req);
        let corbadoUser = await corbado.authTokens.validate(corbadoAuthToken, clientInfo);
        let username = JSON.parse(corbadoUser.data.userData).username;
        const status = await getUserStatus(username);
        console.log("USER EXISTS: ", status.userStatus);

        // if the user does not yet exist in AWS Cognito, add him in AWS Cognito
        if (status.userStatus === NOT_EXISTS) {
            console.log("CREATING USER...");
            await createUser(username);
        }

        // create an AWS Session
        console.log("GET AWS COGNITO SESSION TOKEN");
        let data = await createSession(username);

        res.json(data);
    } catch (error: any) {
        console.log(error);
        res.status(500).send(error.message);
    }
};
```

Moreover, we add the corresponding new routes to app.ts:

```ts filename="app.ts"
// ...

// Old authentication process for Amazon Cognito
//app.post('/api/auth/signup', signUp);
//app.post('/api/auth/login', login);
app.post("/api/auth/logout", logout);

// Corbado passkey-first authentication
const projectID = process.env.CORBADO_PROJECT_ID;
const apiSecret = process.env.CORBADO_API_SECRET;
const corbadoWebhookUsername = process.env.CORBADO_WEBHOOK_USERNAME;
const corbadoWebhookPassword = process.env.CORBADO_WEBHOOK_PASSWORD;

const config = new Corbado.Configuration(projectID, apiSecret);
config.webhookUsername = corbadoWebhookUsername;
config.webhookPassword = corbadoWebhookPassword;
const corbado = new Corbado.SDK(config);

app.post("/api/corbado/webhook", corbado.webhooks.middleware, json(), handleWebhook);
app.get("/api/corbado/authTokenValidate", json(), authTokenValidate);

app.get("/ping", (req: Request, res: Response) => {
    res.send("pong");
});

const port = process.env.PORT || 3000;

app.listen(port, () => {
    console.log(`Server started on port ${port}`);
});
```

Furthermore, we add the methods verifyPassword, getUserStatus, createSession and
createUser to authCognitoController.ts as they are required to make Corbado interact with
Cognito.

```ts filename="authCognitoController.ts"
import { Request, Response } from "express";
import {
    AdminCreateUserCommand,
    AdminCreateUserCommandInput,
    AdminGetUserCommand,
    AdminGetUserCommandInput,
    AdminInitiateAuthCommand,
    AdminInitiateAuthCommandInput,
    AdminSetUserPasswordCommand,
    AdminSetUserPasswordCommandInput,
    AttributeType,
    AuthFlowType,
    ChallengeNameType,
    CognitoIdentityProviderClient,
    CognitoIdentityProviderClientConfig,
    GlobalSignOutCommand,
    InitiateAuthCommand,
    InitiateAuthCommandInput,
    InitiateAuthCommandOutput,
    MessageActionType,
    RespondToAuthChallengeCommand,
    RespondToAuthChallengeCommandInput,
    RespondToAuthChallengeCommandOutput,
} from "@aws-sdk/client-cognito-identity-provider";
// @ts-ignore
import crypto from "crypto";
// @ts-ignore
import { hashSecret, validateJWT, generatePassword } from "../utils/helper";
import { EXISTS, NOT_EXISTS } from "../utils/constants";

require("dotenv").config({ path: "../.env" });

const userPoolId = process.env.COGNITO_USER_POOL_ID;
const clientId = process.env.COGNITO_CLIENT_ID || "";
const clientSecret = process.env.COGNITO_CLIENT_SECRET || "";
const region = process.env.COGNITO_REGION || "";
const poolData = { UserPoolId: userPoolId, ClientId: clientId };

const clientConfig: CognitoIdentityProviderClientConfig = {
    region: region,
};
const client = new CognitoIdentityProviderClient(clientConfig);

// old Amazon Cognito code commented out
/*export const signUp = async (req: Request, res: Response) => {
    const {email, password} = req.body;
    // Workaround, as don't want to send out confirm emails
    const paramsAdminCreateUser: AdminCreateUserCommandInput = {
        ...poolData,
        Username: email,
        DesiredDeliveryMediums: ["EMAIL"],
        UserAttributes: [
            {Name: 'email', Value: email},
            {Name: 'email_verified', Value: 'true'},
        ],
        MessageAction: MessageActionType.SUPPRESS
    };
    try {
        const createUserCommand = new AdminCreateUserCommand(paramsAdminCreateUser);
        await client.send(createUserCommand);
        console.log("USER SUCCESSFULLY CREATED");
        const paramsSetUserPassword: AdminSetUserPasswordCommandInput = {
            ...poolData,
            Username: email,
            Permanent: true,
            Password: password
        };
        const confirmUserCommand = new AdminSetUserPasswordCommand(paramsSetUserPassword);
        await client.send(confirmUserCommand);
        console.log("USER SUCCESSFULLY CONFIRMED");
        res.status(200).json({message: 'User created successfully'});
    } catch (err) {
        console.log(err);
        res.status(500).json({message: 'An error occurred'});
    }
};
export const login = async (req: Request, res: Response) => {
    const {email, password} = req.body;
    const authParams = {
        ...poolData,
        AuthFlow: 'USER_PASSWORD_AUTH',
        AuthParameters: {
            USERNAME: email,
            PASSWORD: password,
            SECRET_HASH: ""
        },
    };
    const hash = hashSecret(clientSecret, email, clientId);
    if (hash && authParams.AuthParameters) {
        authParams.AuthParameters.SECRET_HASH = hash;
    }
    try {
        const initiateAuthCommand = new InitiateAuthCommand(authParams);
        const authResult = await client.send(initiateAuthCommand);
        if (!authResult.AuthenticationResult) {
            res.status(401).json({message: "Invalid credentials"});
            return;
        }
        res.status(200).json({token: authResult.AuthenticationResult.AccessToken});
    } catch (err) {
        console.log(err);
        res.status(401).json({message: 'Invalid credentials'});
    }
};*/

export const logout = async (req: Request, res: Response) => {
    const { token } = req.body;
    const params = {
        ...poolData,
        AccessToken: token,
    };
    try {
        const globalSignOutCommand = new GlobalSignOutCommand(params);
        await client.send(globalSignOutCommand);
        res.status(200).json({ message: "User logged out successfully" });
    } catch (err) {
        console.log(err);
        res.status(500).json({ message: "An error occurred" });
    }
};

export const verifyPassword = async (
    email: string,
    password: string,
): Promise<boolean> => {
    if (!(email?.trim() && password?.trim())) {
        console.log("Error with email or password");
        return false;
    }
    const params: InitiateAuthCommandInput = {
        ClientId: clientId,
        AuthFlow: AuthFlowType.USER_PASSWORD_AUTH,
        AuthParameters: {
            USERNAME: email,
            PASSWORD: password,
            SECRET_HASH: hashSecret(clientSecret, email, clientId) || "",
        },
    };
    try {
        const command = new InitiateAuthCommand(params);
        const output = (await client.send(command)) as InitiateAuthCommandOutput;
        return Boolean(output.AuthenticationResult);
    } catch (error) {
        console.log("Error authenticating user:", error);
        return false;
    }
};

interface UserAttributes {
    Name: string;
    Value: string;
}

export const getUserStatus = async (email: string) => {
    const params: AdminGetUserCommandInput = {
        ...poolData,
        Username: email,
    };

    try {
        const command = new AdminGetUserCommand(params);
        const response = await client.send(command);

        // We add a workaround for users, as users who have never been showed a password
        // exist in Cognito (with a random password). Therefore, we introduce the
        // custom variable "createdByCorbado" in the user pool which is stored on
        // every create user event caused by the Corbado web component.

        if (
            !response ||
            !response.UserAttributes ||
            !Array.isArray(response.UserAttributes)
        ) {
            throw new Error("Invalid response object");
        }

        let createdByCorbado: boolean = false;

        response.UserAttributes.forEach((attr: AttributeType | undefined) => {
            if (attr && attr.Name === "custom:createdByCorbado") {
                createdByCorbado = attr.Value === "true";
            }
        });

        // if Corbado has created the user in AWS Cognito, we send back that the user
        // is not an existing user in the sense, he existed prior to Corbado
        return {
            userStatus: EXISTS,
            createdByCorbado: createdByCorbado,
        };
    } catch (error: any) {
        if (error.name === "UserNotFoundException") {
            return {
                userStatus: NOT_EXISTS,
                createdByCorbado: false,
            };
        } else {
            throw error;
        }
    }
};

interface SessionInfo {
    email: string;
    cognitoUUID: string;
    name: string;
    expires: number;
    idToken: string;
    refreshToken: string;
}

export const createSession = async (username: string): Promise<SessionInfo> => {
    console.log("START CREATE SESSION");
    const params: AdminInitiateAuthCommandInput = {
        ...poolData,
        AuthFlow: AuthFlowType.CUSTOM_AUTH,
        ClientId: clientId,
        AuthParameters: {
            USERNAME: username,
            SECRET_HASH: hashSecret(clientSecret, username, clientId) || "",
        },
    };

    try {
        const adminInitiateAuthCommand = new AdminInitiateAuthCommand(params);
        const response = await client.send(adminInitiateAuthCommand);

        let answer = "FAILURE";
        if (response.ChallengeParameters) {
            answer = response.ChallengeParameters.challenge;
        }

        console.log("ANSWER:", answer);
        const respondToAuthChallengeCommand: RespondToAuthChallengeCommandInput = {
            ClientId: clientId,
            ChallengeName: ChallengeNameType.CUSTOM_CHALLENGE,
            ChallengeResponses: {
                ANSWER: answer,
                USERNAME: username,
                SECRET_HASH: hashSecret(clientSecret, username, clientId) || "",
            },
            Session: response.Session,
        };

        const AuthChallengeCommand = new RespondToAuthChallengeCommand(
            respondToAuthChallengeCommand,
        );
        const authResult = (await client.send(
            AuthChallengeCommand,
        )) as RespondToAuthChallengeCommandOutput;

        if (authResult?.AuthenticationResult) {
            const token = await validateJWT(
                authResult.AuthenticationResult.IdToken as string,
            );
            const user: any = token as any;

            return {
                email: user.email as string,
                cognitoUUID: user.sub,
                name: user.name,
                expires: user.exp,
                idToken: authResult.AuthenticationResult.IdToken as string,
                refreshToken: authResult.AuthenticationResult.RefreshToken as string,
            };
        } else throw Error("Failed to create session");
    } catch (error) {
        throw error;
    }
};

export const createUser = async (email: string) => {
    const params: AdminCreateUserCommandInput = {
        ...poolData,
        Username: email,
        DesiredDeliveryMediums: ["EMAIL"],
        UserAttributes: [
            { Name: "email", Value: email },
            { Name: "email_verified", Value: "true" },
            { Name: "custom:createdByCorbado", Value: "true" },
        ],
        MessageAction: MessageActionType.SUPPRESS,
    };
    try {
        const createUserCommand = new AdminCreateUserCommand(params);
        const responseCreateUserCommand = await client.send(createUserCommand);

        console.log("USER SUCCESSFULLY CREATED");

        const setPasswordParams: AdminSetUserPasswordCommandInput = {
            ...poolData,
            Username: email,
            Permanent: true,
            Password: generatePassword(15),
        };
        const confirmUserCommand = new AdminSetUserPasswordCommand(setPasswordParams);
        await client.send(confirmUserCommand);
        console.log("USER SUCCESSFULLY CONFIRMED");

        return responseCreateUserCommand;
    } catch (error: any) {
        console.log("Error during user creation: ", error);
        return false;
    }
};
```

To better structure our code, we add a utils folder and constants.ts

```ts filename="constants.ts"
export const EXISTS = "exists";
export const NOT_EXISTS = "not_exists";
export const BLOCKED = "blocked";
```

as well as helper.ts

```ts filename="helper.ts"
// @ts-ignore
import crypto from "crypto";
// @ts-ignore
import jwt from "jsonwebtoken";
import { int } from "aws-sdk/clients/datapipeline";
// @ts-ignore
import jwkToPem, { JWK } from "jwk-to-pem";

require("dotenv").config({ path: "../.env" });

const clientId = process.env.COGNITO_CLIENT_ID;
const region = process.env.COGNITO_REGION;
const envJWKS = process.env.COGNITO_JWKS;
const userPoolId = process.env.COGNITO_USER_POOL_ID;
const jwks: JWK[] | any[] = JSON.parse(envJWKS as string);

export function hashSecret(clientSecret: string, username: string, clientId: string) {
    if (!clientSecret) {
        return null;
    }
    return crypto
        .createHmac("SHA256", clientSecret)
        .update(username + clientId)
        .digest("base64");
}

export function validateJWT(jwtToken: string, skipExpiredCheck?: boolean) {
    let res;
    try {
        let pem = jwkToPem(jwks[0]);
        res = jwt.verify(jwtToken, pem, { algorithms: jwks[0].alg });
    } catch (error) {
        let pem = jwkToPem(jwks[1]);
        res = jwt.verify(jwtToken, pem, { algorithms: jwks[1].alg });
    }

    if (!jwtToken.trim()) {
        console.log("Error with JWT");
        return;
    }
    let decoded = jwt.decode(jwtToken);
    const now = +new Date() / 1000;
    // @ts-ignore
    if (!skipExpiredCheck && decoded.exp < now) {
        console.log("Token expired");
        return;
    }
    // @ts-ignore
    if (decoded.aud !== clientId) {
        console.log("Invalid audience in token");
        return;
    }
    // @ts-ignore
    if (decoded.iss !== `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`) {
        console.log("Invalid iss in token");
        return;
    }
    return res;
}

export function generatePassword(length: int) {
    const charset =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+~`|}{[]:;?><,./-=";
    let password = "";
    for (let i = 0; i < length; i++) {
        const randomIndex = Math.floor(Math.random() * charset.length);
        password += charset[randomIndex];
    }
    return password;
}
```

That's it, you have successfully made all necessary integration steps.

If you want to test your application locally, you need to use the Corbado CLI (check the
[docs for the quick setup](https://docs.corbado.com/products/corbado-connect/connect-via-webhooks/corbado-cli)).
We run the CLI with:

```bash
corbado subscribe http://localhost:3000
```

The reason behind is that Corbado, needs to send a webhook request to your local
application, which is by default not reachable from the public. Corbado CLI creates a
tunnel, so that your local application can receive these webhooks. If you're developing on
a live server (e.g. staging), the Corbado CLI is not required.

Accessing [http://localhost:4200](http://localhost:4200) in our browser, should now
display the following screen after entering the email:

1. Existing password-based users want to log in:

![Backend Integration Passkeys Password User](https://www.corbado.com/website-assets/6458a28131db955aff477b81_backend_integration_1_bd072a9bff.png)

2. New users with passkey-ready devices:

![Backend Integration Passkey Popup Touch ID](https://www.corbado.com/website-assets/64ef4b58d74a05062b5490c3_backend_integration_2_c106ac9d43.png)

3. New users with non-passkey-ready devices:

![Email Magic Link Fallback Passkey](https://www.corbado.com/website-assets/6458a2ae34d354cfd9543f40_backend_integration_3_c6e5507fa0.png)

## 5. Learnings from passwordless auth with Amazon Cognito

- We first thought about using the ADMIN_NO_SRP_AUTH flow for creating sessions, as this
  flow didn't require a password to be set, but the flow was deprecated in
  [September 2021](https://docs.aws.amazon.com/sdk-for-ruby/v2/api/Aws/CognitoIdentityProvider/Types/CreateUserPoolClientRequest.html).
  This made using a CUSTOM_AUTH flow the only viable alternative.
- Another thing that you need to consider when choosing Amazon Cognito as user management
  system, is that you cannot export password hashes and thus are more or less bound to
  Amazon Cognito forever. This makes switching to another user management provider nearly
  impossible (as long as you want to stick to some degree with password-based
  authentication).
- Currently, the integration of other authentication providers into Amazon Cognito is not
  as seamless as it could be. Especially, the following things are inferior:
    - We only need the AWS Lambda functions to create a session for passwordless users.
      Therefore, needing 3 AWS Lambda functions is quite an overhead.
    - The real authentication happens even before the Lambda functions are triggered.
- The developer experience of Amazon Cognito is often quite bad, e.g. the documentation of
  Amazon Cognito was sometimes outdated which makes developing quite hard, especially for
  non-standard cases. Moreover, error logs often do not really provide meaningful messages
  to debug properly. Besides, it sometimes takes some time until you see the logs in
  Amazon CloudWatch.
- New users are automatically set to FORCE_PASSWORD_CHANGE. Directly updating the user
  doesnt work with AdminUpdateUserAttributes or AdminConfirmSignUpCommand, so we had to
  execute the AdminSetUserPasswordCommand and provide a randomly generated password to
  confirm the new user in Amazon Cognito.
- When executing some admin commands, we sometimes faced error messages that our Cognito
  user pool did not exist, or we were not authorized to interact with it. Often, the cause
  of these messages is missing or wrong credentials in your .aws/credentials file. Here,
  you need to provide aws_access_key_id and aws_secret_access_key, which you can obtain in
  the following way
    - Login to your AWS account
    - Navigate to the AWS Management Console
    - Click on your username in the top right corner and select Security Credentials
    - Scroll down Access keys
    - Click Create access key to create a new access key.
    - Confirm and you can display or download the new access key.

## 6. Opinion on AWS' passwordless sample

While doing the integration, AWS released a first prototype of an own in-house
[passwordless sample application](https://github.com/aws-samples/amazon-cognito-passwordless-auth).
Just having a look at the code repository showed that the code base and adaptions to make
on your own are massive. The code is not well documented. Its clearly not ready for
production and without the CDK file or large AWS experience, the setup will be a
nightmare. It took us quite long to get it up and running, but we managed it eventually.
It's also not completely for free as AWS Key Management Service (KMS) is used, where
[one KMS key currently is $1 per month](https://aws.amazon.com/de/kms/pricing/). Also,
youre heavily locked into AWS infrastructure, so integrating it into your own tech stack
seemed to be quite laborious.

## 7. Summary: Amazon Cognito Passkeys

In this tutorial, we've learned how to successfully integrate passkey authentication into
an existing Amazon Cognito setup with Corbado. We leveraged Amazon Cognito custom auth
flows to integrate the external authentication provider and showed the setup required to
smoothly transition existing users to passkeys, while still offering passwords as
fallback.

Due to our experiences with Amazon Cognito: if you are building a new application or
website, we wouldn't recommend going with Cognito as its clearly not in the focus of AWS
and does not provide a passkey-first or passwordless-first experience. This is according
to our and
[Google's believe](https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password/)
essential, especially on mobile or native apps.

_Other useful resources:_

- [https://theburningmonk.com/2023/03/passwordless-authentication-made-easy-with-cognito-a-step-by-step-guide/](https://theburningmonk.com/2023/03/passwordless-authentication-made-easy-with-cognito-a-step-by-step-guide/)
- [https://gist.github.com/mobilequickie/f4b4da3e42ae49d2306156ba3a9eaa75](https://gist.github.com/mobilequickie/f4b4da3e42ae49d2306156ba3a9eaa75)
- [https://stackoverflow.com/questions/63043865/aws-cognito-sign-in-without-password](https://stackoverflow.com/questions/63043865/aws-cognito-sign-in-without-password)
- [https://medium.com/@robert.broeckelmann/openid-connect-authorization-code-flow-with-aws-cognito-246997abd11a](https://medium.com/@robert.broeckelmann/openid-connect-authorization-code-flow-with-aws-cognito-246997abd11a)
- [https://jamesmiller.blog/how-to-add-amazon-cognito-auth-to-a-web-app-part-1/ ](https://jamesmiller.blog/how-to-add-amazon-cognito-auth-to-a-web-app-part-1/)
- [https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html)

## Frequently Asked Questions

### Why do I need AWS Lambda functions to add passkeys to Amazon Cognito?

Amazon Cognito does not natively support external
[passkey providers](https://www.corbado.com/blog/passkey-providers) without custom authentication flows. Three
Lambda functions are required: define auth challenge, create auth challenge and verify
auth challenge response. These allow an external provider like Corbado to hook into
Cognito's session creation process.

### What happens to my existing password-based users when I integrate passkeys into Amazon Cognito?

Existing password-based users can continue logging in with their passwords during the
transition. After their first login post-integration, they are offered the option to
[create a passkey](https://www.corbado.com/blog/passkey-creation-best-practices), which then becomes the
preferred method while the password remains as a fallback.

### What is the difference between native Amazon Cognito passkeys and a dedicated passkey layer like Corbado?

Native Cognito passkeys treat passkeys as one option among many and do not actively
encourage adoption, which typically results in low uptake among non-power users. Corbado
is a passkey-first layer designed to maximize adoption through optimized UX flows and is
particularly suited for large existing user bases with custom-built frontends.

### Can I migrate away from Amazon Cognito after building my authentication on it?

Migrating away from Amazon Cognito is extremely difficult because the platform does not
allow you to export password hashes. This creates a hard dependency on Cognito for any
users authenticated with passwords, making switching to another identity provider nearly
impossible without forcing a
[password reset](https://www.corbado.com/blog/password-reset-increase-customer-retention).

### How much does AWS's own open-source passwordless Cognito sample cost to run?

AWS's passwordless Cognito sample uses AWS Key Management Service (KMS), which costs USD 1
per KMS key per month. Beyond cost, the sample requires substantial AWS and CDK expertise
to configure and is noted as not production-ready out of the box.
