back arrowBack to Blog

Developers

Adding Passkeys to Your Amazon Cognito User Pool With Descope

Cognito blog post thumbnail

Amazon Cognito has been a popular authentication and user management solution for developers to add user sign up, sign in, and access control to their web and mobile apps. However, using Amazon Cognito with password-based authentication or similar methods can cause user friction and churn in case they forget their password. It also puts the onus on you as the app developer to protect against account takeover, credential stuffing, and other security concerns that stem from having passwords.

Descope recently announced OIDC federated authentication support: this capability allows developers to easily add passkeys and other passwordless methods to their Amazon Cognito user pools without making any changes to your app’s code.

Follow the steps in this blog to configure your Amazon Cognito app to use Descope passkeys so that your users don’t have to worry about remembering their password anymore!

What does this mean for you?

Here's a glimpse of what this integration can bring to your application:

  • Simplified user management: With Descope as an OIDC provider, users can authenticate with passkeys while Amazon Cognito handles the user pool management and sync across multiple devices. This simplifies managing user identities and saves precious time for your development team.

  • Enhanced security: Descope's integration with Amazon Cognito IdP ensures your application and user data is backed by a passwordless, secure authentication process, minimizing the risk of data breaches and unauthorized access.

  • Improved user experience: With auth methods such as passkeys, your users enjoy a smoother, faster and seamless login experience, increasing their satisfaction and engagement with your app.

How it works

With the power of OpenID Connect, Descope acts as a federated identity provider to handle user authentication while Amazon Cognito acts as the primary identity provider and the user identity information store.

With this integration, new users are automatically created in the user pool. For existing users that want to sign in with passkeys rather than passwords, an AWS Lambda function will trigger to merge user identities in Amazon Cognito and retain all necessary roles and permissions.

The simplified flow diagram below shows this process:

Cognito OIDC Diagram
Fig: Descope as federated IdP with Amazon Cognito

For developers, this process takes place seamlessly, requiring minimal configuration and management effort. This frees up your development resources to focus on your application's unique functionality rather than user authentication.

Now that you understand what we’re trying to accomplish, let’s dive into how you can implement this in your own user pool.

Setting up your Descope Flow

As a reminder, Descope Flows are drag-and-drop workflows that help developers build authentication with a few lines of code. This section goes through how you can create a Descope Flow to properly handle passkey authentication.

In our sample app repository, you can download the oidc-flow JSON which you can import into your own project. It is important to use this Flow, as it is designed to make sure the user and their email is always verified when using passkeys as an authentication method for security reasons.

adding flows
Fig: Descope Flows page where you import the JSON file

You will also need to embed the flow component using either React or a HTML Web Component in your application, and specify in the Descope Console where it is hosted. This is important because Cognito will need to know where you redirect you once you select Login with Passkeys on your main application login page.

descope console oidc configuration
Fig: Flow Hosting URL is the URL of where your Flow is embedded

Note: You should keep this page open, as you’re going to need this information for the next parts of this blog.

If you would like to edit the UI of the passkey login screen, you can do that in the Flow Editor. Once your flow is complete and your login redirect has been configured, you’ll need to connect your Flow to Amazon Cognito by setting Descope up as an external provider.

Descope as an external provider

In order to set up Descope as an external provider with Amazon Cognito, you’ll first need to create a user pool in Amazon Cognito, if you don’t already have one. Most of the people reading this blog will probably have configured one already, but in case you have not, all you need to do is make sure that your user pool requires the email attribute and allows a sign in with email option.

required attributes - email
Fig: Make sure your user pool has email as a required attribute
user pool sign in options - email
Fig: Make sure your user pool allows users to sign in with email

Once you have a configured user pool, you’ll need to set up Descope as an external provider. You can do that by following these steps:

  • In the Descope Console under Authentication Methods -> SSO -> OpenID Connect, you'll need a few things in order to configure the external provider. Fetch all of this information and put it in the respective fields in the configuration page.

    • Provider Name: Descope

    • Client ID: Your Descope Project ID found under Project

    • Client Secret: API key generated under Access Keys

    • Issuer URL: URL with all of the OIDC standard information to complete the authentication process. This can be found on the Authentication Methods screen. If you’re using a CNAME with Descope, make sure that the Issuer URL contains your custom domain name, rather than api.descope.com (e.g. https://example.company.com/<Project ID>)

  • Under Authorized Scopes, add the following scopes: openid profile email descope:custom_claims. The descope:custom_claims scope will allow us to include additional info, such as roles / permissions, tenants, and custom claims in the JWT to return back to Cognito.

  • Make sure that the Attribute Request Method is GET. There’s no need to add an Identifier. At this point, your configuration screen should look something like this:

oidc configuration
Fig: Setting up OIDC federation in Cognito
  • Under Retrieve OIDC Endpoints, select the Auto fill through Issuer URL option and paste in the Issuer URL you got from the Console in step 2:

issuer url
Fig: Where the issuer URL is located
  • Finally, you’ll need to map your attributes between Descope and Amazon Cognito in the following fashion:

oidc configuration attribute mapping
Fig: Mapping attributes between Cognito and Descope

Here, you can edit the custom claims that come back to Amazon Cognito after Descope authentication is complete if you wish. You will need to map the corresponding key in the Custom Claim action (in the Flow) with the attributes mapped here.

Custom Claims Action
Fig: Custom Claims action in a Descope Flow

If you’re using the Cognito Hosted UI, you should now automatically see an option to sign in with Descope:

cognito hosted ui
Fig: Cognito Hosted UI with Descope login

However, if you’re using a custom UI (as I suspect most of you are), then you’ll need to add some way – like the Descope button you see above – to navigate to where you've embedded the Descope Flow component in your application. The purpose of this is to give your users the ability to log in with how you've configured your Flow. To do this, make sure that your button hits the Cognito OAuth /authorize endpoint to start the OIDC process.

This is the general URL structure of the AWS Cognito GET endpoint: https://<user pool name>.auth.<aws region id>.amazoncognito.com/oauth2/authorize?

With the query parameters:

  • identity_provider=Descope (OIDC Provider Name set in Cognito)

  • &redirect_uri=<Redirect URI (when login is successful)>

  • &response_type=CODE

  • &client_id=<Cognito App Client ID>

  • &scope=email openid phone

You can learn more about how to configure the OAuth /authorize endpoint in the Amazon Cognito Documentation.

Once you have your external provider configured and your login screen working, you should be able to sign in with Descope and complete the flow by adding passkeys. All of the OIDC logic will work in the background. Your users should be able to sign in with passkeys or continue using the traditional authentication methods defined by your user pool.

So we’re done right? Well, almost…

Setting up an AWS Lambda trigger

Since we’re using Descope as a federated IdP, the users will not automatically merge together. This means that if a user logs in with Descope and logs in with Amazon Cognito, two separate users with differing permissions and roles will be configured in the user pool. We can handle this issue by using an AWS Lambda trigger to merge the user identities, so that the same user can use either method of login and gain access to the same account.

To configure this user merge functionality follow the steps below:

  • Head to the User Pool Properties tab in your Amazon Cognito dashboard, and under Lambda Triggers, select Add Lambda Trigger. This will open up a new tab.

  • Configure your Lambda trigger to look like the screenshot below, and then select Create a Lambda Trigger:

create sign up lamdba trigger
Fig: Create a Lambda trigger
  • Select Create Function in the top right corner, and configure the following items:

    • Function name: OIDC_USER_MERGE

    • Runtime: Select Python

  • Create the function, and then in the Code section, paste the code snippet shown below. This function will also print out the needed event and user information when merging identities, so that you can track it in your AWS CloudWatch logs.

import boto3

client = boto3.client('cognito-idp')

def lambda_handler(event, context):
    print("Event: ", event)
    email = event['request']['userAttributes']['email']

    # Find a user with the same email
    response = client.list_users(
        UserPoolId=event['userPoolId'],
        AttributesToGet=[
            'email',
        ],
        Filter='email = "{}"'.format(email)
    )

    print('Users found: ', response['Users'])

    for user in response['Users']:
        provider = None
        provider_value = None
        # Check which provider it is using
        if event['userName'].startswith('descope_'):
            provider = 'Descope'
            
            provider_value = event['request']['userAttributes']['name']

        print('Linking accounts from Email {} with provider {} provider_value {} '.format(
            email,
            provider,
            provider_value
        ))

        # If the signup is coming from a social provider, link the accounts
        # with admin_link_provider_for_user function
        if provider and provider_value:
            print('> Linking user: ', user)
            print('> Provider Id: ', provider_value)
            response = client.admin_link_provider_for_user(
                UserPoolId=event['userPoolId'],
                DestinationUser={
                    'ProviderName': 'Cognito',
                    'ProviderAttributeValue': user['Username']
                },
                SourceUser={
                    'ProviderName': provider,
                    'ProviderAttributeName': 'Cognito_Subject',
                    'ProviderAttributeValue': provider_value
                }
            )
    
    # Return the event to continue the workflow
    return event
  • Finally, go back to the original tab you had open, make sure that the Lambda trigger is selected, and add it to your user pool with the Add Lambda trigger button:

create lamdba trigger
Fig: Add Lambda trigger to user pool

We’re almost there! Now all we have to do is make sure that the Lambda trigger has the correct permissions configured to be able to access the user identities through the SDK.

Adding permissions for Lambda trigger

In order to use this Lambda trigger to merge identities, the SDK being used will need to have access to your user pool and all of the identities stored in it. If you try logging in with Descope at this time, you’ll notice that the user merging fails because of a permission issue.

To resolve this, you’ll need to create a new identity permissions policy in the AWS IAM Console, and make sure that Lambda trigger role is assigned to that new policy.

You can do that by following the steps below:

  • Head to your IAM Console, select Policies, and click on the blue Create New Policy button in the top right hand corner.

  • Under Select a Service, select Cognito User Pools.

  • Give permission to all Cognito User Pool actions, and make sure you specify what Resource ARNs you will need for the user merging process. In the example below, I’ve selected all, but you will most likely only need to access to the userpool ARN.

iam policy permissions configurations
Fig: Defining permissions in AWS IAM
  • After clicking Next, your final screen should look something like the screenshot below. Add a name and description to the policy and click on Create Policy.

creating iam policy second step
Fig: Creating an AWS IAM policy
  • Now head to the Roles section of the IAM Console, search for the OIDC_PROD_MERGE (or whatever you decided to name the Lambda trigger you created in the previous section) and select it.

roles list in iam console
Fig: Roles list in AWS IAM
  • Select Attach policies under Add permissions.

attach policy to oidc prod merge role
Fig: Add permissions to your role
  • Search for the policy you created, and add it as a permission policy to this specific role. After that, your Lambda trigger will have full access to all of the user identity information and will be able to use the Amazon Cognito SDK to perform the merging tasks.

Now, whenever you sign in with Descope, it will automatically check to see if the user identity already exists in the user pool based on the user’s email associated with their biometrics. If a user already exists, all relevant properties will be merged.

You don’t need to have any security concerns with merging user identities with this Lambda function. Since user email verification is enforced whenever biometrics is used, a malicious user won’t be able to set up biometrics with someone else’s email and sign in that way. The Flow will never complete unless the email account is verified.


mic drop gif

With Descope and OpenID Connect, you can now easily bid adieu to the era of passwords and step into a future of secure, user-friendly passkey authentication for your Amazon Cognito user pool. 

If you’re interested in seeing how this is implemented in a sample React application, feel free to check out our sample app on GitHub. If you are an Amazon Cognito customer and want to explore adding passkeys to your authentication flow, sign up for our platform and keep this blog bookmarked!

Having trouble with the steps in this blog, or have questions about Descope in general? We’d love to have you over at AuthTown, our open user community for developers to learn more about authentication.

Until then, happy coding!