Today, we will cover how to integrate passkey authentication, the new standard
for web authentication, into an existing Amazon Cognito instance that
currently authenticates its users with passwords. As passkeys are quite novel,
not many tutorials exist on how to integrate them with existing
infrastructure. During the creation of this tutorial, AWS released an own
passwordless prototype, which is a heavy construct and not ready for
production (as AWS says).
The frontend of the sample application uses
Angular, while the backend
runs on Node.js / Express both written in
TypeScript. In general, you can use any web tech stack to integrate passkeys.
We use Corbados passkey-first web 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.
We're currently reworking the Corbado Connect approach. Reach out if you want to get early access.
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 at1. Setup of Amazon Cognitoand3. Setup AWS Lambda functions with custom auth flowsto
check these settings. These AWS Lambda functions are used by Corbado to hook
into the authentication flow, as Amazon Cognito heavily relies on passwords
(theres not even a way to export password hashes), so a fully passwordless
setup out-of-the-box is otherwise not possible in Amazon Cognito.
Become part of our Passkeys Community for updates and support.
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:
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.
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.
I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.
10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We’ve written 150+ blog posts on passkeys.
To keep things simple, just use the Amazon Cognito email service:
Define a user pool name:
Select "Confidential client" as requests to Amazon Cognito will be made via
our Node.js backend:
Review everything and create the user pool:
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 project structure (only most important files are described below, see
GitHub repository for
full code):
The frontend is generated with Angular CLI version 15.2.7. If not done yet,
install it via:
Install all other packages by running the following command in the ./frontend-
angular directory:
You should see the following screen when you go to http://localhost:4200 in
your browser:
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
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 for full
code):
Install all required packages by running the following command in the
./backend-nodejs directory:
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 > Navigation bar
COGNITO_USER_POOL_ID: Amazon Cognito > User pools
COGNITO_CLIENT_ID: Amazon Cognito > User pools > corbado-user-pool > App
integration (on the bottom)
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):
Start the local development server that runs on port 3000:
If everything works fine, you should see the following output in the terminal:
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.
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 login 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 (the email confirmation should be skipped for now).
Subscribe to our Passkeys Substack for the latest news, insights and strategies.
Create the first function, name it like defineCorbadoAuthChallenge and
add the following code. It basically triggers the custom authentication flow.
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).
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).
Afterwards, you should see three AWS Lambda functions in your AWS Lambda
console:
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, its easy to
handle:
Open Log groups and click on the AWS Lambda function you want to check:
Click on the log stream where you want to see details:
Now you can see the log events:
3.2 Add AWS Lambda triggers to user pool in
Amazon Cognito
Add an AWS Lambda trigger at Amazon Cognito > User pools > Corbado-user-pool >
User pool properties:
Click on Add Lambda trigger
Select Custom authentication
Select Define auth challenge
Select defineCorbadoAuthChallenge
Create two additional Lambda triggers for the two remaining Lambda functions
in the same way:
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:
Click on "Sign-up experience":
Click on "Add custom attributes":
Add the new attribute with name createdByCorbado, leave all other
setting on default and click on Save changes (unfortunately, theres
no Boolean datatype).
4. Integrate Corbado into your
application
Now everything is set up to add Corbado to the app. The flow looks as follows:
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).
The user is created / checked in Amazon Cognito.
AWS Lambda functions handle the custom auth flow and generate a session for the user.
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 create 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:
Existing password-based users created prior to passkeys integration want to login: We let them login 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.
New users with passkey-ready devices: We create a passkey for them directly in the first step and will use this passkey for subsequent logins. As fallback method, email magic links are used.
New users with non-passkey-ready devices: To stay future-proof, 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:
We click on Integration guide to do everything step by step:
We integrate via Web component, so thats what we select.
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 > Integration guide. This guides us
through all the steps necessary for the integration to work.
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 request sharing). In our
case, we set it to http://localhost:4200.
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 with a webhook username and
webhook password as credentials, so we can already enter that here.
In step 3, we add our Application URL, Redirect URL and Relying Party ID. The Application URL is the
URL in the frontend where the web component runs. For example, its 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 later, but we can enter it here already.
The 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 ID. As we test locally, we set
the Relying Party ID to
localhost.
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:
Then, we replace the existing login / sign-up components with the Corbado web
component.
Next, we add the CUSTOM_ELEMENTS_SCHEMA to app.module.ts.
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:
Lastly, we add the corbadoAuthTokenValidate function to our AuthService:
4.3 Backend integration
At first, we add Corbado environment variables to our .env file:
(Optional) CORBADO_CLI_SECRET: obtain it from here
Then, we install the required Corbado packages with:
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.
Moreover, we add the corresponding new routes to app.ts:
Furthermore, we add the methods verifyPassword, getUserStatus,
createSession and createUser to authCognitoController.ts as they
are required to make Corbado interact with Cognito.
To better structure our code, we add a utils folder and constants.ts
as well as helper.ts
Thats 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). We run the CLI with:
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 youre developing on a live server (e.g. staging), the Corbado
CLI is not required.
Accessing http://localhost:4200 in our browser, should now display the
following screen after entering the email:
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 didnt require a password to be set, but the flow was deprecated in September 2021. 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 at 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. 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. Also, youre heavily locked
into AWS infrastructure, so integrating it into your own tech stack seemed to
be quite laborious.
Summary
In this tutorial, weve 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 wouldnt 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 Googles believe essential, especially on mobile or native apps.