Skip to main content
Version: 2.0.0-beta

Cognito

Passwordless FIDO2 Authentication with Amazon Cognito

This guide demonstrates how to use a custom authentication flow to set up LoginID passwordless FIDO2 authentication.

Overview:

Cognito Pools allow the use of custom authentication flows to set up a challenge/response-based authentication model using AWS Lambda Functions. The Lambda Functions call LoginID APIs to Authenticate users without passwords.

Overview Setup:

1.0 Setup a LoginID Tenant with Dashboard

For this integration you will need the following items from your LoginID tenant settings:

  1. Web Tenant base URL
  2. Tenant level ES256 private key needed to call backend services

Here are the steps to setup your LoginID tenant and get the required settings:

  1. Head over to our customer dashboard and login or create your account.
  2. Once logged into the dashboard, go to this URL: https://dashboard.gen2.qa.loginid.io/tenants/add. This will take you to the advanced create tenant page.
  1. Make sure Non OIDC tenant is selected.
  2. Enter a name for your tenant in the Tenant Name field.
  3. Enter the RPID (Relying Party ID) value of your hosted application.
  4. Enter the Allowed Origins url of your hosted application.
  1. Click on the Advanced Configuration tab and click the Generate Key Pair button

This will create an ES256 key pair where the public key is stored with your LoginID tenant. Your private key will be shown here once. Copy the PEM formatted private key to a temporary secure location as it will be needed later.

  1. Finish creating your tenant and copy the base URL.

2.0 Setup AWS Cognito Lambdas for Passkeys

When working with CloudFormation, the template can facilitate the setup process by automatically generating the required configurations. Additionally, it can serve as a reference point for manually configuring services, as the template provides detailed information for each service.

To accomplish this you will require a Cognito user pool along with a user client. You will also need to set up three Lambda triggers which include: DefineAuthChallenge, CreateAuthChallenge, and VerifyAuthChallenge.

To create a custom authentication process, the DefineAuthChallenge, CreateAuthChallenge, and VerifyAuthChallenge functions are utilized. These functions work in tandem to generate a personalized challenge for the user to respond to (CreateAuthChallenge), which is then verified in the VerifyAuthChallenge Lambda. The client would receive the assertion options as the challenge and provide a signed assertion as the answer in this scenario.

Secrets manager is also utilized here as a best practice to safely store the tenant ES256 private key that was generated by LoginID.

It is also important to note that this template can only be used for new user pools.

2.1 Using CloudFormation (setup with new Cognito User Pool)

The required AWS services can be created using AWS CloudFormation, which can be accessed here.

Create CloudFormation Stack

aws cloudformation create-stack \
--stack-name myteststack \
--template-body file:///path/to/template/Temaplate.yaml \
--parameters ParameterKey=LOGINIDBaseURL,ParameterValue=[tenant base url from dashboard] ParameterKey=LOGINIDPrivateKey,ParameterValue=[private key]

2.2 Using CloudFormation (setup with existing Cognito User Pool)

Most of the required AWS services can be created using AWS CloudFormation, which can be accessed here.

aws cloudformation create-stack \
--stack-name myteststack \
--template-body file:///path/to/template/TemaplateExisting.yaml \
--parameters ParameterKey=LOGINIDBaseURL,ParameterValue=[tenant base url from dashboard] ParameterKey=LOGINIDPrivateKey,ParameterValue=[private key] ParameterKey=CognitoUserPoolID,ParameterValue=[Existing Cognito user pool ID] ParameterKey=CognitoUserPoolARN,ParameterValue=[Existing Cognito user pool ARN]

After the resources are created successfully, the ARNs of the trigger Lambdas will be displayed as output (DefineAuthArn, CreateAuthArn, VerifyAuthArn). You can retrieve them using

aws cloudformation describe-stacks --stack-name myteststack

Then add the Lambdas as triggers to your existing user pool.

aws cognito-idp update-user-pool \
--user-pool-id [Existing user pool ID]\
--lambda-config DefineAuthChallenge=[DefineAuthArn],CreateAuthChallenge=[CreateAuthArn],VerifyAuthChallengeResponse=[VerifyAuthArn] \
--auto-verified-attributes=[Your list of auto verified attributes of your user pool. Usually only ‘email’]

2.3 Setup AWS Cognito manually (Existing User Pools)

2.3.1 Add LoginID Private Key to Secret Manager

We will use the secret manager service to keep our private key safe. The key was provided from the LoginID dashboard Tenant settings. We need it to create and sign authorized tokens for backend API calls.

Here, we simply paste the private key as plain text.

It's important to observe that the newlines are replaced with’ \n’.

The default Lambda scripts are designed to take care of this process.

Add a name to your secret.

Finish up the rest of the settings and copy the secret name.

2.3.2 Add/Update Custom Authentication Lambda Scripts

As mentioned earlier, your Cognito user pool will use custom authentication, necessitating the configuration of three Lambda function triggers: DefineAuthChallenge, CreateAuthChallenge, and VerifyAuthChallenge.

In case you haven't configured these triggers, you can find:

You can copy and utilize the code, incorporating the dependency layer into your respective Lambda functions. If your user pool already has these three triggers in place, you will need to implement the necessary logic and update the Lambda accordingly. The provided Lambda scripts in the S3 bucket serve as a helpful reference.

In the following section, we'll illustrate how to create the Lambda functions from scratch and guide you through the implementation process.

Adding Python SDK Layer

This layer is necessary for the Lambdas to work smoothly without encountering runtime errors. You can obtain it from the S3 bucket and upload it in the Layers section of the Lambda service.

  • Click Create layer
  • Give a name for your layer (LoginID Python SDK)
  • Upload the dependency by clicking the Upload button
  • Select Python 3.8 as the Compatible runtimes
  • Click Create

We will be using the layer for 2 of the Lambda functions (CreateAuthChallenge and VerifyAuthChallenge).

DefineAuthChallenge

Head over to Lambdas service and click on Create function.

  • Select Python 3.8 Runtime
  • Click Create function

Copy and paste the DefineAuthChallenge.py code from the S3 bucket and click Deploy.

The Lambda does not need a dependency layer or any environment variables.

Go to your user pool and add it as the DefineAuth trigger.

  • Click User pool properties tab
  • Click Add Lambda trigger
  • Select Custom Authentication and Define auth challenge
  • Click Add Lambda trigger

CreateAuthChallenge

The process is almost identical to the DefineAuthChallenge Lambda section, but this Lambda will need the dependency layer and environment variables set.

  • Select Python 3.8 Runtime
  • Click Create function
  • Copy and paste the CreateAuthChallenge.py code from the S3 bucket
  • Click Deploy

You will now need to add the layer.

  • Scroll to the bottom and click Add a layer.
  • Select Custom layers
  • Select the Python SDK layer and version
  • Click Add

Add the environment variables.

  • Click Configuration tab
  • Click Environment variables tab
  • Click Edit
  • Add the following variables:
    • LOGINID_BASE_URL: The LoginID base URL obtained after creating a Web Tenant.
    • LOGINID_SECRET_NAME: The name of the secret created to store the ES256 private key obtained from LoginID.

Finally, go to your user pool and add it as the CreateAuth trigger.

VerifyAuthChallenge

Setting up this Lambda follows the same process as the CreateAuthChallenge section, so ensure you follow those steps.

Note: Please make sure to copy the VerifyAuthChallenge.py code from the S3 bucket.

In summary, ensure the following:

  • Copy VerifyAuthChallenge.py code to this Lambda
  • Add Python SDK dependency layer
  • Add the environment variables (LOGINID_BASE_URL and LOGINID_SECRET_NAME)
  • Add it as VerifyAuth trigger to your user pool

3.0 Setup Client SDK (Frontend)

The last step is to install the front-end WebSDK, which handles passkeys and custom authentication seamlessly. Behind the scenes, the SDK utilizes the amazon-cognito-identity-js library for Cognito flows. You can install the SDK via a script tag.

<script src="https://s3.us-west-1.amazonaws.com/cognito-loginid/index.min.js"></script>

User Add Passkey

To add a passkey, a Cognito user must exist and be signed in (authenticated). This is necessary as the ID token is required for the process. The ID token will be verified before completing the addition of a new passkey.

Below is a code sample demonstrating where the call can be made, typically in a button handler.

const { COGNITO_CLIENT_ID, COGNITO_USER_POOL_ID } = config;
const Loginid = new LoginIDCognitoWebSDK(
COGNITO_USER_POOL_ID,
COGNITO_CLIENT_ID
);

const handlerSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

try {
const token = await cognito.getUserIDToken();
await Loginid.addPasskey(username, token);
} catch (e: any) {
setError(e.message);
}
};

Note On Sign Up With Passkey

Signing up with a passkey can be achieved using addPasskey(username, idToken). As Cognito mandates users to have a password, you can randomly generate and assign a password to the user during sign-up. The concept is to retain it and use it for the initial sign-in, allowing the user ID token to be obtained after confirmation. From there, addPasskey(username, idToken) can be used. This simulates a sign-up experience with a passkey.

User Sign In Passkey

Signing in with a passkey only requires the username; the SDK will take care of the rest. Here's a sample code snippet.

const { COGNITO_CLIENT_ID, COGNITO_USER_POOL_ID } = config;
const Loginid = new LoginIDCognitoWebSDK(
COGNITO_USER_POOL_ID,
COGNITO_CLIENT_ID
);

const handlerSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

try {
await Loginid.signInPasskey(username);
} catch (e: any) {
setError(e.message);
}
};