AWS Cognito (OIDC)

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. An AWS Lambda function will then trigger to merge user identities in Amazon Cognito and retain all necessary roles and permissions.

The simplified flow diagram below shows this process:

Descope OIDC guide diagram of AWS Cognito as auth provider

Follow the steps in this guide to configure your Amazon Cognito app to use Descope Flows.

Setting up your Descope Flow

Note

If you want to use Passkeys, you can download the oidc-flow JSON from our sample app repository, 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.

Descope OIDC with AWS Cognito as auth provider flow configuration 1

Your flows are automatically hosted with our Descope Auth Hosting Application. To learn more about our hosted app, you can read about it in our Docs page here.

If you're using the oidc-flow.json provided above, edit the query parameter at the end of the Flow Hosting URL like so: https://auth.descope.io/<Project ID>?flow=oidc-flow

Descope OIDC with AWS Cognito as auth provider flow configuration 2

Note

You should keep this page open, as you're going to need this information for the next parts of this guide.

If you would like to edit the UI of the 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

AWS Cloud Formation Script

If you wish to use a pre-built Cloud Formation script to setup Descope as an external provider, rather than following the steps individually, you can use the following script:

This script will perform the following actions:

  • Conditional User Pool Creation - The script can either use an existing Cognito User Pool or create a new one based on the input provided.
  • Descope Integration - It sets up Descope as an external identity provider, configuring it with necessary credentials and settings.
  • Identity Federation Handling Through the Lambda function, it handles scenarios where a user may have identities in both Descope and Cognito, merging these to maintain a consistent user identity across different login methods.
  • IAM Role Configuration for Lambda - Ensures that the Lambda function has the necessary permissions to execute its intended operations, particularly those involving interactions with the Cognito User Pool.

Note

Unless you're intimately familiar with Cloud Formation and understand how to use these scripts, it's recommended to perform the steps listed below manually one-by-one. That way you can control the configuration in a more granular fashion.

Copy this lambda, and change the following values to use it:

  1. Parameters - You will need to specify the following items:
    • Descope Project ID - Your Project ID from Project Settings
    • Descope Access Key - An Access Key secret generated under Access Keys
    • Cognito User Pool ID (Optional) - If you want to apply this configuration to a previously defined Cognito user pool, include the ID in your cloud formation script
    • Issuer URL from Descope - The issuer URL of Descope (as an OIDC provider). This can be found under Applications -> Your OIDC app -> Issuer

Descope Issuer URL

  1. User Pool and Client Properties - You need to specify the properties for the Cognito User Pool and User Pool Client according to your requirements.
  2. Lambda Function Code - The Lambda function code should be written in Python and embedded in the ZipFile property under the OIDCUserMergeLambda resource. Alternatively, you can upload the code to an S3 bucket and reference it in the template.
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template for configuring Cognito with Descope.
 
Parameters:
  DescopeClientId:
    Type: String
    Description: Descope Project ID
  DescopeClientSecret:
    Type: String
    Description: Descope Access Key
  IssuerURL:
    Type: String
    Description: Issuer URL from Descope
  ExistingUserPoolId:
    Type: String
    Default: ""
    Description: Existing Cognito User Pool ID (leave blank to create a new one)
 
Conditions:
  CreateNewUserPool: !Equals [!Ref ExistingUserPoolId, ""]
 
Resources:
  UserPool:
    Type: "AWS::Cognito::UserPool"
    Condition: CreateNewUserPool
    Properties:
      # User Pool properties
 
  UserPoolClient:
    Type: "AWS::Cognito::UserPoolClient"
    Properties:
      # User Pool Client properties
 
  IdentityProvider:
    Type: "AWS::Cognito::UserPoolIdentityProvider"
    Properties:
      UserPoolId: !If [CreateNewUserPool, !Ref UserPool, !Ref ExistingUserPoolId]
      ProviderName: "Descope"
      ProviderType: "OIDC"
      ProviderDetails:
        client_id: !Ref DescopeClientId
        client_secret: !Ref DescopeClientSecret
        attributes_request_method: "GET"
        oidc_issuer: !Ref IssuerURL
        authorize_scopes: "openid profile email descope.custom_claims descope.claims"
      # Additional properties
 
  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "lambda.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "CognitoLambdaExecutionPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "cognito-idp:*"
                Resource: "*"
 
  OIDCUserMergeLambda:
    Type: "AWS::Lambda::Function"
    Properties:
      FunctionName: "OIDC_USER_MERGE"
      Runtime: "python3.8"
      Handler: "index.lambda_handler"
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          import boto3
          client = boto3.client('cognito-idp')
          def lambda_handler(event, context):
              # Please include your custom lambda trigger code here (e.g. the example one shown below)
 
Outputs:
  UserPoolId:
    Description: ID of the Cognito User Pool
    Value: !If [CreateNewUserPool, !Ref UserPool, !Ref ExistingUserPoolId]
  UserPoolClientId:
    Description: ID of the Cognito User Pool Client
    Value: !Ref UserPoolClient

Important Notes:

  • IAM Role and Policies - The LambdaExecutionRole is configured to allow the Lambda function to perform actions on Cognito User Pools. Adjust the permissions according to your security requirements.
  • Testing and Security - Test this template in a controlled environment before deploying it in production. Ensure that all sensitive information is handled securely.

Once you've run this code, Descope should be configured as an external provider, and you should be good to go! If you're interested in viewing a sample application of Cognito working with Descope, you can skip to the bottom of this doc

Creating / Using a User Pool

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. Just make sure that your user pool requires the email attribute and allows a sign in with email option.

Descope OIDC with AWS Cognito as auth provider cognito configuration 1

Descope OIDC with AWS Cognito as auth provider cognito configuration 2

Once you have a configured user pool, you'll need to set up Descope as an external provider.

What you'll need from the Descope Console

In the Descope Console, 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: Call it Descope
  • Issuer URL: This is the Issuer URL which is found under your Authentication Methods -> SSO -> Identity Provider configuration settings
  • Client ID: Your Descope Project ID, which can be found under Project Settings in the Descope Console
  • Client Secret: Access key generated under Access Keys in the Descope Console

Creating the External Provider in Cognito

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

  2. Make sure that the Attribute Request Method is GET. There's no need to add an Identifier.

Note

If custom claims or roles are not passed in from Descope, but if they are configured to be accepted in your Cognito configuration, then the previous value will be saved in the Cognito user and not be replaced. As an example, if you're mapping roles but then the use in Descope originally has a role and then all role information is removed. In that case, the original role information will persist in Cognito. However, new values such as an updated Role name will replace the original value.

At this point, your configuration screen should look something like this:

Descope OIDC with AWS Cognito as auth provider cognito configuration 3

  1. 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 (e.g. https://api.descope.com/<Project ID>)

  2. Finally, you'll need to map your attributes between Descope and Amazon Cognito in the following fashion:

Descope OIDC with AWS Cognito as auth provider cognito configuration 4

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.

Descope OIDC with AWS Cognito as auth provider flow configuration 5

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

Descope OIDC with AWS Cognito testing completed flow

However, if you're using a custom UI (as I suspect most of you are), then you'll need to add a way to start the OIDC flow. To start the OIDC flow, you'll need to navigate to the OAuth /authorize endpoint, as explained here. Here is an example URL:

https://sample-app-prod.auth.us-west-2.amazoncognito.com/oauth2/authorize?identity_provider=Descope&redirect_uri=https://app.example.com/dashboard&response_type=CODE&client_id=cognito_app_client_id&scope=email%20openid%20phone

Once you have your external provider configured and your login screen working, you should be able to sign in with Descope, based on how you've configured your Flow. All of the OIDC logic will work in the background.

Your users should be able to sign in using your personalized Descope Flow 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:

Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 1

  • 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:

Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 2

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.

Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 3

  • 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.

Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 4

  • 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.

Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 5

  • Select Attach policies under Add permissions.

Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 6

  • 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 coming from Descope. If a user already exists, all relevant properties will be merged.

Note

If you are using AWS Lambda triggers to merge the user identity, make sure in your Flow that the user has been verified by OTP, Magic Link, or by signing in with OAuth Providers like Google or Facebook. The reason for this, is to ensure that the Descope user account you're merging with the one in Cognito is indeed associate with that same user. In the oidc-flow.json Flow provided for Descope Passkeys above, for example, the user's email is verified before the flow is completed.

Sample App

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 have any other questions about Descope or our flows, feel free to reach out to us!

Was this helpful?

On this page